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 d2701f8341..3f4a076654 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 3f4a076654..e0da345bc5 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 ce8d10d655..04abdf8c1c 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 1bbceee9af..9685d86b40 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 b48fa5b56d..94a849b0d0 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 7af363d596..fd22dc466e 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 1dc88d2e71..c6ab7c6ebb 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 78403444ff..5bed935319 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 165cfcf190..e60c1ff907 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 bbd8d0393c..2d33ba45e5 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 a87dc3e773..4e8cd4bc15 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 46916c74f1..0d540a47b4 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 3b95feb9ef..656847980c 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 42a3824bfc..7058068453 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 1fb935b2b8..1f4f2f40a1 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 857f3f56fc..3000b9575d 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 310b3c25eb..7ac6c95f3e 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 df200eae7f..e87c065917 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 c011649b2e..268f3ccb1c 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 d9cf537333..80555c7442 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 4c6298d8f5..5454519905 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 1b8142d964..6005277f73 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 0f7e4a35c6..56e3d10de3 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 da4b3473b3..2e252cfaab 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 196c04d203..8f9d1e4d05 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 9792f16cbe..c4d58cd7a7 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 66654fbe93..9357c31f19 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 f21b1c363c..0510baa9e4 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 8344914da0..b9bfadb17f 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 3b4a94dd0e..b003042866 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 e29beb3ad5..9c402d139a 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 2972c307f2..928c9b23f8 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 e60f6deb2f..4a90d92b35 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 6fc1467566..3def545d71 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 b9bf74ee85..cb46e74af0 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 bcd3f76dca..3fa0e003e7 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 e6e1cff598..b650d272fb 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 b650d272fb..b2b8b2e4bd 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 b2b8b2e4bd..0be52646e6 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 8a4c04d338..957c5e1c06 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 b4145edb64..5270d6f951 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 832e776264..deab45d4b2 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 7c6a213d0d..6c558cbb09 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 a1f126bfb8..942f61d298 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 acf5a9d72b..4eedade69d 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 fe44a3d2aa..d3e44edfe9 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 0000000000..0d1065089f --- /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 6c558cbb09..b4a8f13bea 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 942f61d298..637f3f4487 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 4eedade69d..58d34649c7 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 a0b26bff92..6c8c615015 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 0000000000..7a101e6b09 --- /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 c952ef3a4e..677406931e 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 0ccf9c2bb7..67791aaecb 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 67791aaecb..9ea75d0730 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 0000000000..d4b3375b0d --- /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 cb46e74af0..750beaea13 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 b4a8f13bea..f6a85aa70c 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 637f3f4487..3c7a6ff5df 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 677406931e..3f7a850a57 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 58d34649c7..0411a8e5ec 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 e9de52745a..19acb949e2 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 0000000000..db277775b3 --- /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 4934bc494d..87986f1032 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 adc55f8c91..57fde112bf 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 7af363d596..da601ba351 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 5429305098..45842aa561 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 58cda0c7dc..18e3915527 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 d584c575d2..614866d9c9 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 98d8cb8749..474f2128fc 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 57fde112bf..38ec8ffb40 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 87307264f5..1b4e32f4b8 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 18e3915527..f1711afec2 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 3f7a850a57..643db8c798 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 643db8c798..cef23862c5 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 bd114de216..23c75f4014 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 5270d6f951..a4d9259a6d 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 e44ab3e33a..2a1fef6a56 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 da601ba351..b2d732d170 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 f0c1699f16..c076a8a476 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 2fa8699883..0fa64f531e 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 b2d732d170..adabcf0a8b 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 c076a8a476..74563d05bc 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 0fa64f531e..80f1037466 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 e4beb58873..9dc5d99bc5 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 2a78d37cbb..77d011490e 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 f78a67ddb3..50003020e9 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 8062a12b83..ae3e609b93 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 6a59cecae8..9794ac45c1 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 63076ba234..3d4ea3d6f2 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 b0795818b8..9e30796bbc 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 efd6ac145e..12ab12dc27 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 614866d9c9..9375c4e78d 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 4b0bf1cd4c..b67bf0bab4 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 a00b073586..fbf4e750c6 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 a5877aaccb..95d6445d17 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 803cb5cc85..fec8f27c97 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 adabcf0a8b..9c0e50647c 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 f3e0c9529a..a72c5468ed 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 5245e53a05..064ec61a37 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 fbc7d83f82..845a63bd1b 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 a040b1d053..bcfa01ca5c 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 3c94ba4061..eca372d577 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 83f99861c5..e271d8583c 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 3c523c2487..16ad20746d 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 a8fd7eb45a..0e779d195c 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 5d445a7b82..4de229f104 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 756d396510..a3311fb6d4 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 3456f59a04..0567320675 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 d9801015b7..7a69a818ef 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 2378b8449b..e4503c6fbb 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 1b63005cbf..b503bcb28d 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 7d2ed0a225..9084e373f2 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 7761bbb3a2..36ab21ca1e 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 e00d0b9557..f061bbf8f9 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 fb85d997fa..fcb087c47d 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 725729337b..57abd8ea2b 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 b31bddaeb5..71ab963d82 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 304b10a018..2c0caf8935 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 188067078d..604e052a63 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 f5129f8f31..957b20ecdb 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 93cf2608f4..2610cfa1cc 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 54dbfa00bd..7240173a89 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 78fde3752f..57ebea8539 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 201b0e9276..40ba61cd05 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 188067078d..604e052a63 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 17719b325c..8c1ac9fcd0 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 9272108670..bf086ea156 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 3a50ac5371..16e95d90d5 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 4d262f8146..17575663a1 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 3c52b6d989..fd6aa18ced 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 13dfce6d77..6887fc7c30 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 9e7f9acfc3..0c43e7010e 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 68ec01c92b..58b2a1cbf2 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 ee0aa133a0..ccfdd60a98 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 7df5434f91..cfc1f8d813 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 deab45d4b2..9ad008f38d 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 0349359533..b1bd96e165 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 317f600b1e..28ae854787 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 51958af145..e239f9a840 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 45baf4e871..6261939994 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 fe0c53e87d..177a8c5bd8 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 6df36d7077..d2701f8341 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 db8f98de66..540a1d0ff9 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 f5f8359074..bc8f07425f 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 d930793669..f759bc8139 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 9a0029581c..b76ccc5b60 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 8704455886..2bd0bbf259 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 95e45efa95..2fa6b58faa 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 97f3922f36..f672b892ee 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 6554287578..c3de7826c9 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 decccc0a6d..5c5482a754 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 eb21d2418b..0000000000 --- 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 9b23dbde3c..0000000000 --- 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 a1913027b7..281bd74b21 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 b1d0bedb2c..4804f5b0f1 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 ab94f16a07..1a8e6d938d 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 4066b5b229..a9c5e697a5 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 fea6118bdf..03ab91623b 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 7461fb28c7..7d4b4126b7 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 fec8f27c97..659bee7445 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 d0cd437946..2444465be6 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 eb03d2bdc0..96921643d4 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 750beaea13..dc40e7fb85 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 63076ba234..089cbb2995 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 0fe49d4d04..f53115f224 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 bac70f139a..c7ea1b4f38 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 b6701f12d6..2c9d9c0c71 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 9c0e50647c..45169ed3af 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 9dc5d99bc5..7cdcbc2c8f 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 d4a16b5758..ce05a417ad 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 0be52646e6..fdc5ea108a 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 cbf914230d..8471f3a3a7 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 45842aa561..1e635432bd 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 1b4e32f4b8..e57edd3b14 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 e108a05ccc..874978b4fc 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 ee658c9cc9..98883fac33 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 434975216a..3222ea2eb8 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 4b0bf1cd4c..1770c5648e 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 45169ed3af..bd615522c2 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 6261939994..d35b721c6c 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 bd615522c2..9b847e9c0c 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 80f1037466..5c7947a448 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 7cdcbc2c8f..671d2b38c7 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 7c8fe12aa0..b99977a60e 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 2694fa1697..f2692b96db 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 d3ecfb74fb..f915cb24ed 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 5bf9ba6ccf..7a09ac259f 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 5df0ed12e9..7e0b240570 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 96921643d4..a114cd437b 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 dc40e7fb85..2d4b273f5e 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 089cbb2995..47a27c049f 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 2c9d9c0c71..12bf324262 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 9b847e9c0c..dce0b0e5f0 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 5c7947a448..0165c52e9f 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 671d2b38c7..0149c310da 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 ce05a417ad..d26000ebc7 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 fdc5ea108a..021ea2d3bc 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 e57edd3b14..0db66b4e37 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 874978b4fc..a600046ac2 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 3222ea2eb8..2efa9f8daa 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 1770c5648e..a459122cfc 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 1a8e6d938d..037f97f71a 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 a9c5e697a5..770f5d5795 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 03ab91623b..5be7e33bf3 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 7d4b4126b7..935358c2a1 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 659bee7445..0d5bb28ed6 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 2444465be6..f2db0a7763 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 a114cd437b..e8913505ca 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 f53115f224..d25ca7bb88 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 c7ea1b4f38..ac519764fd 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 12bf324262..84a45b083e 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 dce0b0e5f0..90f910d255 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 74563d05bc..f0c1699f16 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 0165c52e9f..3b574eb03e 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 0149c310da..789341d46f 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 021ea2d3bc..80847f9f4b 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 8471f3a3a7..4bd186fc98 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 1e635432bd..265e4f0206 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 0db66b4e37..febd564050 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 2efa9f8daa..09e4c4c219 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 a459122cfc..1c65317c41 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 0d5bb28ed6..d03a2d393b 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 90f910d255..d98033820b 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 a4d9259a6d..ab952ba6e7 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 e8913505ca..11d64f085c 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 fbf4e750c6..5afa1ebe62 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 bc1c904404..2fdeee56c1 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 ab952ba6e7..c70a1a8e04 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 c70a1a8e04..cd5e86b910 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 11d64f085c..7e39647ac6 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 d98033820b..62d7d1d48a 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 789341d46f..20992bfbaa 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 80b8753d80..d03897a071 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 fbf4e750c6..6cd36252d1 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 6e79c6b657..1938e832e9 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 037f97f71a..1a8e6d938d 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 770f5d5795..a9c5e697a5 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 5be7e33bf3..03ab91623b 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 935358c2a1..7d4b4126b7 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 f2db0a7763..2444465be6 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 e8913505ca..a114cd437b 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 d25ca7bb88..f53115f224 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 ac519764fd..c7ea1b4f38 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 84a45b083e..12bf324262 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 31b7db008d..5bd7d03184 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 62d7d1d48a..f967f13403 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 3b574eb03e..5b005d7d6f 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 20992bfbaa..cc8468edbd 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 80847f9f4b..021ea2d3bc 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 4bd186fc98..8471f3a3a7 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 265e4f0206..1e635432bd 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 febd564050..0db66b4e37 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 09e4c4c219..37fab1ee46 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 46fcc014a1..a8a287fb4e 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 f967f13403..c2d2397d9c 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 a2ac13984b..1d8908b8fd 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 5b005d7d6f..dcbe810804 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 c2d2397d9c..730d0da5a0 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 dcbe810804..0331c7922e 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 cc8468edbd..d960b9da16 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 8ee9f7eeff..d7c65b11fa 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 37fab1ee46..c5e0e7e1ad 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 125e14d445..100ab27571 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 6b905a1faa..1aa2a2dd40 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 97549b0058..3c7f84fec7 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 7e0b240570..f231669197 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 4e89f5cabd..188239a8e0 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 cd5e86b910..8f8511deb6 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 2d4b273f5e..9ef95ec929 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 8f8511deb6..c12bdc9c55 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 c12bdc9c55..682974d61f 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 9ef95ec929..735abbbd13 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 682974d61f..0711e5eb02 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 735abbbd13..c3c8681bcf 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 0711e5eb02..8f4f97ad60 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 c3c8681bcf..30a9cba85c 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 8f4f97ad60..17195cb22b 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 30a9cba85c..33ebd4c7bd 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 1a8e6d938d..92ccee91c0 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 03ab91623b..d5e7c877f7 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 3a1dde072f..a39ffc457a 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 a114cd437b..9a65e2e953 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 f53115f224..83aaa3b703 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 c7ea1b4f38..118cddaa92 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 769f2eda55..fa42a52374 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 730d0da5a0..b95cb0179a 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 d960b9da16..70d02c1fa1 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 60e63f9823..4fef6bd715 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 639a7594f1..afd60af848 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 ea09e55091..839ab3aafc 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 c5e0e7e1ad..679c34611b 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 a8a287fb4e..2b9321b303 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 b95cb0179a..bd3fbb8149 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 0331c7922e..675d8b8528 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 679c34611b..dc52614ecf 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 92ccee91c0..29dcd95eae 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 9a65e2e953..61fa952f94 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 675d8b8528..e298224caa 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 021ea2d3bc..07aaea812a 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 dc52614ecf..da708c6ea7 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 2b9321b303..3435727b1e 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 e298224caa..e0fd0cb8e9 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 4804f5b0f1..febe43ce5e 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 29dcd95eae..8ad7864adf 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 7d4b4126b7..f51a6c9238 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 2444465be6..16a7340fae 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 c9688011a4..ec1cc12498 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 e0fd0cb8e9..ff988607b0 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 8471f3a3a7..e88aee5dcf 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 1e635432bd..b20ffed6d7 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 0db66b4e37..dffd88db14 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 a9c5e697a5..6f86a74300 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 17195cb22b..02cd58524b 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 8ad7864adf..ce7fd8a094 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 428f2156d1..a2b281856d 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 d5e7c877f7..63922f2b65 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 f51a6c9238..b3703aa64a 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 f58afab361..75cfd872b2 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 12bf324262..523d6e8a5c 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 fa42a52374..1d3b44fa43 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 ff8e343c2c..e0f46b036a 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 ff988607b0..b1729d89b8 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 70d02c1fa1..44a3c5cfdc 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 07aaea812a..df4334a19f 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 b20ffed6d7..c3b4f5caa6 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 dffd88db14..202d494560 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 4fef6bd715..893f5e8a85 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 afd60af848..5d54a66723 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 839ab3aafc..5eaaef900e 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 ebaf399e22..5bb0fd93f8 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 98883fac33..ecbe25acca 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 da708c6ea7..f099032265 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 3435727b1e..6397a1e6b2 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 b3703aa64a..0d398fcce9 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 828e730403..ddfed0c858 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 df4334a19f..f7582f1764 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 6ad321c735..d00324cf16 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 7146ab7b85..2d3fe8ea83 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 e0f46b036a..e44ac93818 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 1d8908b8fd..67531a8297 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 44a3c5cfdc..df3f820755 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 7bd9ce8aec..db5996829f 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 06befd5f4e..f6cfe5ae30 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 d00324cf16..6be8a547cd 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 a600046ac2..970e0115df 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 f0af080d4a..67827b7803 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 ae95838a74..72117e1cab 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 2a1fef6a56..bb30cac6d3 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 bb30cac6d3..795c83aba6 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 e44ac93818..2b3a3f3bf4 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 f099032265..f2a7a442f4 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 2b3a3f3bf4..ddec0e10a3 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 ddec0e10a3..5eb4ed744c 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 6397a1e6b2..1cde8d5b9a 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 a2b281856d..4978452384 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 0d398fcce9..101d4dc545 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 75cfd872b2..a5534b6262 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 5eb4ed744c..e6e11ca321 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 202d494560..018ab9cb11 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 5bb0fd93f8..e33c0a5391 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 f2a7a442f4..2f884b0ceb 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 1cde8d5b9a..8ad8080375 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 67827b7803..21956be446 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 72117e1cab..7a470db7c9 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 e6e11ca321..507cbffad2 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 f0c1699f16..63905326fe 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 b1729d89b8..6d593c2e72 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 df3f820755..d39219e65d 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 d7c65b11fa..bdc871a802 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 67f8e52c04..1d93a45fc7 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 509c979b85..277ff8403f 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 c0f0ade7b9..59324f473a 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 f949a6cd78..6c11817b5c 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 507cbffad2..8020147844 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 d39219e65d..1012d7e77e 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 59324f473a..2a722cad82 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 308ea6f2d7..a16c3cb21e 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 8020147844..8682e6c9dd 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 342c1c66d0..92ab53f15e 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 277ff8403f..9a799c3a3a 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 2a722cad82..022346ea67 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 789341d46f..889dd2510e 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 ca2cc04b9d..cf39ac782f 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 c0f0ade7b9..83469b8295 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 100ab27571..223f5679ae 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 f231669197..e7e6e0ac72 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 d03897a071..8c4513b250 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 0000000000..c35c4c618f --- /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 8ad8080375..099d837ba4 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 054b283791..f6437d9007 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 c35c4c618f..0000000000 --- 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 795c83aba6..50630fb02a 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 0000000000..c35c4c618f --- /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 467798ebf5..a0be9756bf 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 95d6445d17..1a2575dd5f 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 2f884b0ceb..1612aadc2f 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 099d837ba4..f27a6f075d 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 957c5e1c06..0a18266d2a 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 dbcbaf6072..fefb1c608f 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 4978452384..24ecec08e7 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 5576451b1b..eae1746a01 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 c106f042b5..0ae55e99d9 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 02cd58524b..8d8b77ea95 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 beb5284031..d197f57fa5 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 03a2cd51ed..2133296e25 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 5bed935319..bc903fe74c 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 0510baa9e4..14f3809e67 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 6be8a547cd..770cae907c 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 8064478964..1463ac2155 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 69394b8421..945ac7b0f5 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 a0be9756bf..1ff9981a6a 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 bc903fe74c..56a712802b 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 76daeae2a8..9ff340299b 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 dbf57b83e5..2e6c725872 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 0000000000..b5a91a7e98 --- /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 24ecec08e7..b8024e2bfd 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 0ea4ff758b..24b4811b61 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 92ed9ef77d..afc13f983d 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 0000000000..5d796111c6 --- /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 656847980c..df6f35caaf 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 24c44bb698..c743f00f92 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 6e0ed1a5f1..9e8ec53887 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 397e7647c4..ac7b9ac208 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 7793a28ee0..ac45e0eb94 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 bf7b6e6aac..232d3c5535 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 1463ac2155..06f13cd52d 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 2d3fe8ea83..165537a1af 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 1612aadc2f..ab4f7286dc 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 e87c065917..034494221d 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 c743f00f92..1cc3d33b2a 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 9e8ec53887..ac8389e1f6 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 ac7b9ac208..cc38f3df7a 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 ac45e0eb94..abc763b73b 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 24b4811b61..13856cc8ef 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 df6f35caaf..984742e818 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 1cc3d33b2a..5fe9e34ee4 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 ac8389e1f6..b73e5f8e98 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 cc38f3df7a..b803094cf6 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 abc763b73b..48649c5791 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 5d796111c6..1f33917529 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 984742e818..698100ab29 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 48649c5791..d75a4bceff 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 d75a4bceff..cbc0a92151 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 ce7fd8a094..657457d592 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 2d75b4d2ce..44fbbf163d 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 44fbbf163d..1dbef165fa 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 d595337428..1f3adaf477 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 1dbef165fa..30f02c17f5 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 30f02c17f5..e792064ec7 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 1f3adaf477..9a6748fa7c 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 e792064ec7..a3edb65128 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 9a6748fa7c..1f3adaf477 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 8d8b77ea95..f65f19cfec 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 a3edb65128..b06a720090 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 b06a720090..d014e71488 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 d014e71488..24aa84ee88 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 24aa84ee88..d6b45629a4 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 d6b45629a4..8be72aec46 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 78c858a90c..e0ae64d806 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -434,7 +434,9 @@ impl LanguageServer { ..Default::default() }), inlay_hint: Some(InlayHintClientCapabilities { - resolve_support: None, + resolve_support: Some(InlayHintResolveClientCapabilities { + properties: vec!["textEdits".to_string(), "tooltip".to_string()], + }), dynamic_registration: Some(false), }), ..Default::default() diff --git a/crates/project/src/lsp_command.rs b/crates/project/src/lsp_command.rs index 08261b64f1..a8692257d8 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(),