From 39887be7d062324b7a81ba3734cc1e18e102e909 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 29 Nov 2023 14:38:27 -0800 Subject: [PATCH 01/90] WIP: layout compiling --- crates/editor2/src/items.rs | 2 +- crates/editor2/src/selections_collection.rs | 3 - crates/terminal2/src/terminal_settings.rs | 9 +- crates/terminal_view2/src/terminal_element.rs | 1946 +++++++++-------- crates/theme2/src/default_colors.rs | 4 + crates/theme2/src/one_themes.rs | 3 + crates/theme2/src/styles/colors.rs | 1 + 7 files changed, 990 insertions(+), 978 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index eca3b99d7807b455ba48ed99ef0287a9ee084abf..179aa8cd6f63b941a13941eea05219aa9d3d526d 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -913,7 +913,7 @@ impl SearchableItem for Editor { fn update_matches(&mut self, matches: Vec>, cx: &mut ViewContext) { self.highlight_background::( matches, - |theme| theme.title_bar_background, // todo: update theme + |theme| theme.search_match_background, cx, ); } diff --git a/crates/editor2/src/selections_collection.rs b/crates/editor2/src/selections_collection.rs index bcf41f135ba44b0562d1351901a452b04a1ef10b..d9f0ec3764c94dff36b36cc0fedc74c7c05e8528 100644 --- a/crates/editor2/src/selections_collection.rs +++ b/crates/editor2/src/selections_collection.rs @@ -315,14 +315,11 @@ impl SelectionsCollection { let line = display_map.layout_row(row, &text_layout_details); - dbg!("****START COL****"); let start_col = line.closest_index_for_x(positions.start) as u32; if start_col < line_len || (is_empty && positions.start == line.width) { let start = DisplayPoint::new(row, start_col); - dbg!("****END COL****"); let end_col = line.closest_index_for_x(positions.end) as u32; let end = DisplayPoint::new(row, end_col); - dbg!(start_col, end_col); Some(Selection { id: post_inc(&mut self.next_selection_id), diff --git a/crates/terminal2/src/terminal_settings.rs b/crates/terminal2/src/terminal_settings.rs index 16ec2869227098ad26d7b5db22b6456b9c4f0edb..1038c6d0616e3b391ea7ae8dea3b68b93f1b3cde 100644 --- a/crates/terminal2/src/terminal_settings.rs +++ b/crates/terminal2/src/terminal_settings.rs @@ -1,4 +1,4 @@ -use gpui::{AppContext, FontFeatures, Pixels}; +use gpui::{px, AbsoluteLength, AppContext, FontFeatures, Pixels}; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use std::{collections::HashMap, path::PathBuf}; @@ -114,12 +114,13 @@ pub enum TerminalLineHeight { } impl TerminalLineHeight { - pub fn value(&self) -> f32 { - match self { + pub fn value(&self) -> AbsoluteLength { + let value = match self { TerminalLineHeight::Comfortable => 1.618, TerminalLineHeight::Standard => 1.3, TerminalLineHeight::Custom(line_height) => f32::max(*line_height, 1.), - } + }; + px(value).into() } } diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 363dd90287ad890bf873f60a400568da8ce786e8..5c4635bac5d238e143153f8a15d22709d8c2170c 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,970 +1,976 @@ -// #![allow(unused)] // todo!() - -// use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; -// use gpui::{ -// point, transparent_black, AnyElement, AppContext, Bounds, Component, CursorStyle, Element, -// ElementId, FontStyle, FontWeight, HighlightStyle, Hsla, IntoElement, IsZero, LayoutId, -// ModelContext, Overlay, Pixels, Point, Quad, TextRun, TextStyle, TextSystem, Underline, -// ViewContext, WeakModel, WindowContext, -// }; -// use itertools::Itertools; -// use language::CursorShape; -// use ordered_float::OrderedFloat; -// use settings::Settings; -// use terminal::{ -// alacritty_terminal::{ -// ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, -// grid::Dimensions, -// index::Point as AlacPoint, -// term::{cell::Flags, TermMode}, -// }, -// terminal_settings::TerminalSettings, -// IndexedCell, Terminal, TerminalContent, TerminalSize, -// }; -// use theme::ThemeSettings; - -// use std::mem; -// use std::{fmt::Debug, ops::RangeInclusive}; - -// use crate::TerminalView; - -// ///The information generated during layout that is necessary for painting -// pub struct LayoutState { -// cells: Vec, -// rects: Vec, -// relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, -// cursor: Option, -// background_color: Hsla, -// size: TerminalSize, -// mode: TermMode, -// display_offset: usize, -// hyperlink_tooltip: Option, -// gutter: f32, -// } - -// ///Helper struct for converting data between alacritty's cursor points, and displayed cursor points -// struct DisplayCursor { -// line: i32, -// col: usize, -// } - -// impl DisplayCursor { -// fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { -// Self { -// line: cursor_point.line.0 + display_offset as i32, -// col: cursor_point.column.0, -// } -// } - -// pub fn line(&self) -> i32 { -// self.line -// } - -// pub fn col(&self) -> usize { -// self.col -// } -// } - -// #[derive(Clone, Debug, Default)] -// struct LayoutCell { -// point: AlacPoint, -// text: Line, -// } - -// impl LayoutCell { -// fn new(point: AlacPoint, text: Line) -> LayoutCell { -// LayoutCell { point, text } -// } - -// fn paint( -// &self, -// origin: Point, -// layout: &LayoutState, -// _visible_bounds: Bounds, -// _view: &mut TerminalView, -// cx: &mut WindowContext, -// ) { -// let pos = { -// let point = self.point; - -// Point::new( -// (origin.x + point.column as f32 * layout.size.cell_width).floor(), -// origin.y + point.line as f32 * layout.size.line_height, -// ) -// }; - -// self.text.paint(pos, layout.size.line_height, cx); -// } -// } - -// #[derive(Clone, Debug, Default)] -// struct LayoutRect { -// point: AlacPoint, -// num_of_cells: usize, -// color: Hsla, -// } - -// impl LayoutRect { -// fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { -// LayoutRect { -// point, -// num_of_cells, -// color, -// } -// } - -// fn extend(&self) -> Self { -// LayoutRect { -// point: self.point, -// num_of_cells: self.num_of_cells + 1, -// color: self.color, -// } -// } - -// fn paint( -// &self, -// origin: Point, -// layout: &LayoutState, -// _view: &mut TerminalView, -// cx: &mut ViewContext, -// ) { -// let position = { -// let alac_point = self.point; -// point( -// (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(), -// origin.y + alac_point.line as f32 * layout.size.line_height, -// ) -// }; -// let size = point( -// (layout.size.cell_width * self.num_of_cells as f32).ceil(), -// layout.size.line_height, -// ) -// .into(); - -// cx.paint_quad( -// Bounds::new(position, size), -// Default::default(), -// self.color, -// Default::default(), -// transparent_black(), -// ); -// } -// } - -// ///The GPUI element that paints the terminal. -// ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? -// pub struct TerminalElement { -// terminal: WeakModel, -// focused: bool, -// cursor_visible: bool, -// can_navigate_to_selected_word: bool, -// } - -// impl TerminalElement { -// pub fn new( -// terminal: WeakModel, -// focused: bool, -// cursor_visible: bool, -// can_navigate_to_selected_word: bool, -// ) -> TerminalElement { -// TerminalElement { -// terminal, -// focused, -// cursor_visible, -// can_navigate_to_selected_word, -// } -// } - -// //Vec> -> Clip out the parts of the ranges - -// fn layout_grid( -// grid: &Vec, -// text_style: &TextStyle, -// terminal_theme: &TerminalStyle, -// text_system: &TextSystem, -// hyperlink: Option<(HighlightStyle, &RangeInclusive)>, -// cx: &mut WindowContext<'_>, -// ) -> (Vec, Vec) { -// let mut cells = vec![]; -// let mut rects = vec![]; - -// let mut cur_rect: Option = None; -// let mut cur_alac_color = None; - -// let linegroups = grid.into_iter().group_by(|i| i.point.line); -// for (line_index, (_, line)) in linegroups.into_iter().enumerate() { -// for cell in line { -// let mut fg = cell.fg; -// let mut bg = cell.bg; -// if cell.flags.contains(Flags::INVERSE) { -// mem::swap(&mut fg, &mut bg); -// } - -// //Expand background rect range -// { -// if matches!(bg, Named(NamedColor::Background)) { -// //Continue to next cell, resetting variables if necessary -// cur_alac_color = None; -// if let Some(rect) = cur_rect { -// rects.push(rect); -// cur_rect = None -// } -// } else { -// match cur_alac_color { -// Some(cur_color) => { -// if bg == cur_color { -// cur_rect = cur_rect.take().map(|rect| rect.extend()); -// } else { -// cur_alac_color = Some(bg); -// if cur_rect.is_some() { -// rects.push(cur_rect.take().unwrap()); -// } -// cur_rect = Some(LayoutRect::new( -// AlacPoint::new( -// line_index as i32, -// cell.point.column.0 as i32, -// ), -// 1, -// convert_color(&bg, &terminal_theme), -// )); -// } -// } -// None => { -// cur_alac_color = Some(bg); -// cur_rect = Some(LayoutRect::new( -// AlacPoint::new(line_index as i32, cell.point.column.0 as i32), -// 1, -// convert_color(&bg, &terminal_theme), -// )); -// } -// } -// } -// } - -// //Layout current cell text -// { -// let cell_text = &cell.c.to_string(); -// if !is_blank(&cell) { -// let cell_style = TerminalElement::cell_style( -// &cell, -// fg, -// terminal_theme, -// text_style, -// text_system, -// hyperlink, -// ); - -// let layout_cell = text_system.layout_line( -// cell_text, -// text_style.font_size.to_pixels(cx.rem_size()), -// &[(cell_text.len(), cell_style)], -// )?; - -// cells.push(LayoutCell::new( -// AlacPoint::new(line_index as i32, cell.point.column.0 as i32), -// layout_cell, -// )) -// }; -// } -// } - -// if cur_rect.is_some() { -// rects.push(cur_rect.take().unwrap()); -// } -// } -// (cells, rects) -// } - -// // Compute the cursor position and expected block width, may return a zero width if x_for_index returns -// // the same position for sequential indexes. Use em_width instead -// fn shape_cursor( -// cursor_point: DisplayCursor, -// size: TerminalSize, -// text_fragment: &Line, -// ) -> Option<(Point, Pixels)> { -// if cursor_point.line() < size.total_lines() as i32 { -// let cursor_width = if text_fragment.width == Pixels::ZERO { -// size.cell_width() -// } else { -// text_fragment.width -// }; - -// //Cursor should always surround as much of the text as possible, -// //hence when on pixel boundaries round the origin down and the width up -// Some(( -// point( -// (cursor_point.col() as f32 * size.cell_width()).floor(), -// (cursor_point.line() as f32 * size.line_height()).floor(), -// ), -// cursor_width.ceil(), -// )) -// } else { -// None -// } -// } - -// ///Convert the Alacritty cell styles to GPUI text styles and background color -// fn cell_style( -// indexed: &IndexedCell, -// fg: terminal::alacritty_terminal::ansi::Color, -// style: &TerminalStyle, -// text_style: &TextStyle, -// text_system: &TextSystem, -// hyperlink: Option<(HighlightStyle, &RangeInclusive)>, -// ) -> TextRun { -// let flags = indexed.cell.flags; -// let fg = convert_color(&fg, &style); - -// let mut underline = flags -// .intersects(Flags::ALL_UNDERLINES) -// .then(|| Underline { -// color: fg, -// thickness: Pixels::from(1.0).scale(1.0), -// order: todo!(), -// bounds: todo!(), -// content_mask: todo!(), -// wavy: flags.contains(Flags::UNDERCURL), -// }) -// .unwrap_or_default(); - -// if indexed.cell.hyperlink().is_some() { -// if underline.thickness.is_zero() { -// underline.thickness = Pixels::from(1.0).scale(1.0); -// } -// } - -// let mut properties = Properties::new(); -// if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { -// properties = *properties.weight(FontWeight::BOLD); -// } -// if indexed.flags.intersects(Flags::ITALIC) { -// properties = *properties.style(FontStyle::Italic); -// } - -// let font_id = text_system -// .select_font(text_style.font_family, &properties) -// .unwrap_or(text_style.font_id); - -// let mut result = TextRun { -// color: fg, -// font_id, -// underline, -// }; - -// if let Some((style, range)) = hyperlink { -// if range.contains(&indexed.point) { -// if let Some(underline) = style.underline { -// result.underline = Some(underline); -// } - -// if let Some(color) = style.color { -// result.color = color; -// } -// } -// } - -// result -// } - -// // todo!() -// // fn generic_button_handler( -// // connection: WeakModel, -// // origin: Point, -// // f: impl Fn(&mut Terminal, Point, E, &mut ModelContext), -// // ) -> impl Fn(E, &mut TerminalView, &mut EventContext) { -// // move |event, _: &mut TerminalView, cx| { -// // cx.focus_parent(); -// // if let Some(conn_handle) = connection.upgrade() { -// // conn_handle.update(cx, |terminal, cx| { -// // f(terminal, origin, event, cx); - -// // cx.notify(); -// // }) -// // } -// // } -// // } - -// fn attach_mouse_handlers( -// &self, -// origin: Point, -// visible_bounds: Bounds, -// mode: TermMode, -// cx: &mut ViewContext, -// ) { -// // todo!() -// // let connection = self.terminal; - -// // let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); - -// // // Terminal Emulator controlled behavior: -// // region = region -// // // Start selections -// // .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { -// // let terminal_view = cx.handle(); -// // cx.focus(&terminal_view); -// // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); -// // if let Some(conn_handle) = connection.upgrade() { -// // conn_handle.update(cx, |terminal, cx| { -// // terminal.mouse_down(&event, origin); - -// // cx.notify(); -// // }) -// // } -// // }) -// // // Update drag selections -// // .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { -// // if event.end { -// // return; -// // } - -// // if cx.is_self_focused() { -// // if let Some(conn_handle) = connection.upgrade() { -// // conn_handle.update(cx, |terminal, cx| { -// // terminal.mouse_drag(event, origin); -// // cx.notify(); -// // }) -// // } -// // } -// // }) -// // // Copy on up behavior -// // .on_up( -// // MouseButton::Left, -// // TerminalElement::generic_button_handler( -// // connection, -// // origin, -// // move |terminal, origin, e, cx| { -// // terminal.mouse_up(&e, origin, cx); -// // }, -// // ), -// // ) -// // // Context menu -// // .on_click( -// // MouseButton::Right, -// // move |event, view: &mut TerminalView, cx| { -// // let mouse_mode = if let Some(conn_handle) = connection.upgrade() { -// // conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift)) -// // } else { -// // // If we can't get the model handle, probably can't deploy the context menu -// // true -// // }; -// // if !mouse_mode { -// // view.deploy_context_menu(event.position, cx); -// // } -// // }, -// // ) -// // .on_move(move |event, _: &mut TerminalView, cx| { -// // if cx.is_self_focused() { -// // if let Some(conn_handle) = connection.upgrade() { -// // conn_handle.update(cx, |terminal, cx| { -// // terminal.mouse_move(&event, origin); -// // cx.notify(); -// // }) -// // } -// // } -// // }) -// // .on_scroll(move |event, _: &mut TerminalView, cx| { -// // if let Some(conn_handle) = connection.upgrade() { -// // conn_handle.update(cx, |terminal, cx| { -// // terminal.scroll_wheel(event, origin); -// // cx.notify(); -// // }) -// // } -// // }); - -// // // Mouse mode handlers: -// // // All mouse modes need the extra click handlers -// // if mode.intersects(TermMode::MOUSE_MODE) { -// // region = region -// // .on_down( -// // MouseButton::Right, -// // TerminalElement::generic_button_handler( -// // connection, -// // origin, -// // move |terminal, origin, e, _cx| { -// // terminal.mouse_down(&e, origin); -// // }, -// // ), -// // ) -// // .on_down( -// // MouseButton::Middle, -// // TerminalElement::generic_button_handler( -// // connection, -// // origin, -// // move |terminal, origin, e, _cx| { -// // terminal.mouse_down(&e, origin); -// // }, -// // ), -// // ) -// // .on_up( -// // MouseButton::Right, -// // TerminalElement::generic_button_handler( -// // connection, -// // origin, -// // move |terminal, origin, e, cx| { -// // terminal.mouse_up(&e, origin, cx); -// // }, -// // ), -// // ) -// // .on_up( -// // MouseButton::Middle, -// // TerminalElement::generic_button_handler( -// // connection, -// // origin, -// // move |terminal, origin, e, cx| { -// // terminal.mouse_up(&e, origin, cx); -// // }, -// // ), -// // ) -// // } - -// // cx.scene().push_mouse_region(region); -// } -// } - -// impl Element for TerminalElement { -// type State = LayoutState; - -// fn layout( -// &mut self, -// element_state: Option, -// cx: &mut WindowContext<'_>, -// ) -> (LayoutId, Self::State) { -// let settings = ThemeSettings::get_global(cx); -// let terminal_settings = TerminalSettings::get_global(cx); - -// //Setup layout information -// let terminal_theme = settings.theme.terminal.clone(); //TODO: Try to minimize this clone. -// let link_style = settings.theme.editor.link_definition; -// let tooltip_style = settings.theme.tooltip.clone(); - -// let text_system = cx.text_system(); -// let font_size = font_size(&terminal_settings, cx).unwrap_or(settings.buffer_font_size(cx)); -// let font_family_name = terminal_settings -// .font_family -// .as_ref() -// .unwrap_or(&settings.buffer_font_family_name); -// let font_features = terminal_settings -// .font_features -// .as_ref() -// .unwrap_or(&settings.buffer_font_features); -// let family_id = text_system -// .load_family(&[font_family_name], &font_features) -// .log_err() -// .unwrap_or(settings.buffer_font_family); -// let font_id = text_system -// .select_font(family_id, &Default::default()) -// .unwrap(); - -// let text_style = TextStyle { -// color: settings.theme.editor.text_color, -// font_family_id: family_id, -// font_family_name: text_system.family_name(family_id).unwrap(), -// font_id, -// font_size, -// font_properties: Default::default(), -// underline: Default::default(), -// soft_wrap: false, -// font_family: todo!(), -// font_features: todo!(), -// line_height: todo!(), -// font_weight: todo!(), -// font_style: todo!(), -// background_color: todo!(), -// white_space: todo!(), -// }; -// let selection_color = settings.theme.editor.selection.selection; -// let match_color = settings.theme.search.match_background; -// let gutter; -// let dimensions = { -// let line_height = text_style.font_size * terminal_settings.line_height.value(); -// let cell_width = text_system.em_advance(text_style.font_id, text_style.font_size); -// gutter = cell_width; - -// let size = constraint.max - point(gutter, 0.); -// TerminalSize::new(line_height, cell_width, size) -// }; - -// let search_matches = if let Some(terminal_model) = self.terminal.upgrade() { -// terminal_model.read(cx).matches.clone() -// } else { -// Default::default() -// }; - -// let background_color = terminal_theme.background; -// let terminal_handle = self.terminal.upgrade().unwrap(); - -// let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { -// terminal.set_size(dimensions); -// terminal.try_sync(cx); -// if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { -// terminal.last_content.last_hovered_word.clone() -// } else { -// None -// } -// }); - -// let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { -// let mut tooltip = Overlay::new( -// Empty::new() -// .contained() -// .constrained() -// .with_width(dimensions.width()) -// .with_height(dimensions.height()) -// .with_tooltip::( -// hovered_word.id, -// hovered_word.word, -// None, -// tooltip_style, -// cx, -// ), -// ) -// .with_position_mode(gpui::OverlayPositionMode::Local) -// .into_any(); - -// tooltip.layout( -// SizeConstraint::new(Point::zero(), cx.window_size()), -// view_state, -// cx, -// ); -// tooltip -// }); - -// let TerminalContent { -// cells, -// mode, -// display_offset, -// cursor_char, -// selection, -// cursor, -// .. -// } = { &terminal_handle.read(cx).last_content }; - -// // searches, highlights to a single range representations -// let mut relative_highlighted_ranges = Vec::new(); -// for search_match in search_matches { -// relative_highlighted_ranges.push((search_match, match_color)) -// } -// if let Some(selection) = selection { -// relative_highlighted_ranges.push((selection.start..=selection.end, selection_color)); -// } - -// // then have that representation be converted to the appropriate highlight data structure - -// let (cells, rects) = TerminalElement::layout_grid( -// cells, -// &text_style, -// &terminal_theme, -// &cx.text_system(), -// last_hovered_word -// .as_ref() -// .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), -// cx, -// ); - -// //Layout cursor. Rectangle is used for IME, so we should lay it out even -// //if we don't end up showing it. -// let cursor = if let AlacCursorShape::Hidden = cursor.shape { -// None -// } else { -// let cursor_point = DisplayCursor::from(cursor.point, *display_offset); -// let cursor_text = { -// let str_trxt = cursor_char.to_string(); - -// let color = if self.focused { -// terminal_theme.background -// } else { -// terminal_theme.foreground -// }; - -// cx.text_system().layout_line( -// &str_trxt, -// text_style.font_size, -// &[( -// str_trxt.len(), -// TextRun { -// font_id: text_style.font_id, -// color, -// underline: Default::default(), -// }, -// )], -// )? -// }; - -// let focused = self.focused; -// TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( -// move |(cursor_position, block_width)| { -// let (shape, text) = match cursor.shape { -// AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), -// AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), -// AlacCursorShape::Underline => (CursorShape::Underscore, None), -// AlacCursorShape::Beam => (CursorShape::Bar, None), -// AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), -// //This case is handled in the if wrapping the whole cursor layout -// AlacCursorShape::Hidden => unreachable!(), -// }; - -// Cursor::new( -// cursor_position, -// block_width, -// dimensions.line_height, -// terminal_theme.cursor, -// shape, -// text, -// ) -// }, -// ) -// }; - -// //Done! -// ( -// constraint.max, -// Self::State { -// cells, -// cursor, -// background_color, -// size: dimensions, -// rects, -// relative_highlighted_ranges, -// mode: *mode, -// display_offset: *display_offset, -// hyperlink_tooltip, -// gutter, -// }, -// ) -// } - -// fn paint( -// self, -// bounds: Bounds, -// element_state: &mut Self::State, -// cx: &mut WindowContext<'_>, -// ) { -// // todo!() -// // let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); - -// // //Setup element stuff -// // let clip_bounds = Some(visible_bounds); - -// // cx.paint_layer(clip_bounds, |cx| { -// // let origin = bounds.origin + point(element_state.gutter, 0.); - -// // // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse -// // self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx); - -// // cx.scene().push_cursor_region(gpui::CursorRegion { -// // bounds, -// // style: if element_state.hyperlink_tooltip.is_some() { -// // CursorStyle::AlacPointingHand -// // } else { -// // CursorStyle::IBeam -// // }, -// // }); - -// // cx.paint_layer(clip_bounds, |cx| { -// // //Start with a background color -// // cx.scene().push_quad(Quad { -// // bounds, -// // background: Some(element_state.background_color), -// // border: Default::default(), -// // corner_radii: Default::default(), -// // }); - -// // for rect in &element_state.rects { -// // rect.paint(origin, element_state, view_state, cx); -// // } -// // }); - -// // //Draw Highlighted Backgrounds -// // cx.paint_layer(clip_bounds, |cx| { -// // for (relative_highlighted_range, color) in -// // element_state.relative_highlighted_ranges.iter() -// // { -// // if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines( -// // relative_highlighted_range, -// // element_state, -// // origin, -// // ) { -// // let hr = HighlightedRange { -// // start_y, //Need to change this -// // line_height: element_state.size.line_height, -// // lines: highlighted_range_lines, -// // color: color.clone(), -// // //Copied from editor. TODO: move to theme or something -// // corner_radius: 0.15 * element_state.size.line_height, -// // }; -// // hr.paint(bounds, cx); -// // } -// // } -// // }); - -// // //Draw the text cells -// // cx.paint_layer(clip_bounds, |cx| { -// // for cell in &element_state.cells { -// // cell.paint(origin, element_state, visible_bounds, view_state, cx); -// // } -// // }); - -// // //Draw cursor -// // if self.cursor_visible { -// // if let Some(cursor) = &element_state.cursor { -// // cx.paint_layer(clip_bounds, |cx| { -// // cursor.paint(origin, cx); -// // }) -// // } -// // } - -// // if let Some(element) = &mut element_state.hyperlink_tooltip { -// // element.paint(origin, visible_bounds, view_state, cx) -// // } -// // }); -// } - -// // todo!() remove? -// // fn metadata(&self) -> Option<&dyn std::any::Any> { -// // None -// // } - -// // fn debug( -// // &self, -// // _: Bounds, -// // _: &Self::State, -// // _: &Self::PaintState, -// // _: &TerminalView, -// // _: &gpui::ViewContext, -// // ) -> gpui::serde_json::Value { -// // json!({ -// // "type": "TerminalElement", -// // }) -// // } - -// // fn rect_for_text_range( -// // &self, -// // _: Range, -// // bounds: Bounds, -// // _: Bounds, -// // layout: &Self::State, -// // _: &Self::PaintState, -// // _: &TerminalView, -// // _: &gpui::ViewContext, -// // ) -> Option> { -// // // Use the same origin that's passed to `Cursor::paint` in the paint -// // // method bove. -// // let mut origin = bounds.origin() + point(layout.size.cell_width, 0.); - -// // // TODO - Why is it necessary to move downward one line to get correct -// // // positioning? I would think that we'd want the same rect that is -// // // painted for the cursor. -// // origin += point(0., layout.size.line_height); - -// // Some(layout.cursor.as_ref()?.bounding_rect(origin)) -// // } -// } - -// impl IntoElement for TerminalElement { -// type Element = Self; - -// fn element_id(&self) -> Option { -// todo!() -// } - -// fn into_element(self) -> Self::Element { -// self -// } -// } - -// fn is_blank(cell: &IndexedCell) -> bool { -// if cell.c != ' ' { -// return false; -// } - -// if cell.bg != AnsiColor::Named(NamedColor::Background) { -// return false; -// } - -// if cell.hyperlink().is_some() { -// return false; -// } - -// if cell -// .flags -// .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) -// { -// return false; -// } - -// return true; -// } - -// fn to_highlighted_range_lines( -// range: &RangeInclusive, -// layout: &LayoutState, -// origin: Point, -// ) -> Option<(Pixels, Vec)> { -// // Step 1. Normalize the points to be viewport relative. -// // When display_offset = 1, here's how the grid is arranged: -// //-2,0 -2,1... -// //--- Viewport top -// //-1,0 -1,1... -// //--------- Terminal Top -// // 0,0 0,1... -// // 1,0 1,1... -// //--- Viewport Bottom -// // 2,0 2,1... -// //--------- Terminal Bottom - -// // Normalize to viewport relative, from terminal relative. -// // lines are i32s, which are negative above the top left corner of the terminal -// // If the user has scrolled, we use the display_offset to tell us which offset -// // of the grid data we should be looking at. But for the rendering step, we don't -// // want negatives. We want things relative to the 'viewport' (the area of the grid -// // which is currently shown according to the display offset) -// let unclamped_start = AlacPoint::new( -// range.start().line + layout.display_offset, -// range.start().column, -// ); -// let unclamped_end = -// AlacPoint::new(range.end().line + layout.display_offset, range.end().column); - -// // Step 2. Clamp range to viewport, and return None if it doesn't overlap -// if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 { -// return None; -// } - -// let clamped_start_line = unclamped_start.line.0.max(0) as usize; -// let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize; -// //Convert the start of the range to pixels -// let start_y = origin.y + clamped_start_line as f32 * layout.size.line_height; - -// // Step 3. Expand ranges that cross lines into a collection of single-line ranges. -// // (also convert to pixels) -// let mut highlighted_range_lines = Vec::new(); -// for line in clamped_start_line..=clamped_end_line { -// let mut line_start = 0; -// let mut line_end = layout.size.columns(); - -// if line == clamped_start_line { -// line_start = unclamped_start.column.0 as usize; -// } -// if line == clamped_end_line { -// line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive -// } - -// highlighted_range_lines.push(HighlightedRangeLine { -// start_x: origin.x + line_start as f32 * layout.size.cell_width, -// end_x: origin.x + line_end as f32 * layout.size.cell_width, -// }); -// } - -// Some((start_y, highlighted_range_lines)) -// } - -// fn font_size(terminal_settings: &TerminalSettings, cx: &mut AppContext) -> Option { -// terminal_settings -// .font_size -// .map(|size| theme::adjusted_font_size(size, cx)) -// } - -// // mappings::colors::convert_color -// fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, style: &TerminalStyle) -> Hsla { -// todo!() -// } +use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; +use gpui::{ + point, px, relative, rems, transparent_black, AnyElement, AppContext, Bounds, Component, + CursorStyle, Element, ElementId, FontStyle, FontWeight, HighlightStyle, Hsla, IntoElement, + IsZero, LayoutId, ModelContext, Overlay, Pixels, Point, Quad, ShapedLine, SharedString, Style, + Styled, TextRun, TextStyle, TextSystem, Underline, UnderlineStyle, ViewContext, WeakModel, + WhiteSpace, WindowContext, +}; +use itertools::Itertools; +use language::CursorShape; +use ordered_float::OrderedFloat; +use settings::Settings; +use terminal::{ + alacritty_terminal::{ + ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, + grid::Dimensions, + index::Point as AlacPoint, + term::{cell::Flags, TermMode}, + }, + terminal_settings::TerminalSettings, + IndexedCell, Terminal, TerminalContent, TerminalSize, +}; +use theme::{ActiveTheme, ThemeColors, ThemeSettings}; + +use std::mem; +use std::{fmt::Debug, ops::RangeInclusive}; + +use crate::TerminalView; + +///The information generated during layout that is necessary for painting +pub struct LayoutState { + cells: Vec, + rects: Vec, + relative_highlighted_ranges: Vec<(RangeInclusive, Hsla)>, + cursor: Option, + background_color: Hsla, + size: TerminalSize, + mode: TermMode, + display_offset: usize, + hyperlink_tooltip: Option, + gutter: Pixels, +} + +///Helper struct for converting data between alacritty's cursor points, and displayed cursor points +struct DisplayCursor { + line: i32, + col: usize, +} + +impl DisplayCursor { + fn from(cursor_point: AlacPoint, display_offset: usize) -> Self { + Self { + line: cursor_point.line.0 + display_offset as i32, + col: cursor_point.column.0, + } + } + + pub fn line(&self) -> i32 { + self.line + } + + pub fn col(&self) -> usize { + self.col + } +} + +#[derive(Debug, Default)] +struct LayoutCell { + point: AlacPoint, + text: gpui::ShapedLine, +} + +impl LayoutCell { + fn new(point: AlacPoint, text: gpui::ShapedLine) -> LayoutCell { + LayoutCell { point, text } + } + + fn paint( + &self, + origin: Point, + layout: &LayoutState, + _visible_bounds: Bounds, + _view: &mut TerminalView, + cx: &mut WindowContext, + ) { + let pos = { + let point = self.point; + + Point::new( + (origin.x + point.column as f32 * layout.size.cell_width).floor(), + origin.y + point.line as f32 * layout.size.line_height, + ) + }; + + self.text.paint(pos, layout.size.line_height, cx); + } +} + +#[derive(Clone, Debug, Default)] +struct LayoutRect { + point: AlacPoint, + num_of_cells: usize, + color: Hsla, +} + +impl LayoutRect { + fn new(point: AlacPoint, num_of_cells: usize, color: Hsla) -> LayoutRect { + LayoutRect { + point, + num_of_cells, + color, + } + } + + fn extend(&self) -> Self { + LayoutRect { + point: self.point, + num_of_cells: self.num_of_cells + 1, + color: self.color, + } + } + + fn paint( + &self, + origin: Point, + layout: &LayoutState, + _view: &mut TerminalView, + cx: &mut ViewContext, + ) { + let position = { + let alac_point = self.point; + point( + (origin.x + alac_point.column as f32 * layout.size.cell_width).floor(), + origin.y + alac_point.line as f32 * layout.size.line_height, + ) + }; + let size = point( + (layout.size.cell_width * self.num_of_cells as f32).ceil(), + layout.size.line_height, + ) + .into(); + + cx.paint_quad( + Bounds::new(position, size), + Default::default(), + self.color, + Default::default(), + transparent_black(), + ); + } +} + +///The GPUI element that paints the terminal. +///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? +pub struct TerminalElement { + terminal: WeakModel, + focused: bool, + cursor_visible: bool, + can_navigate_to_selected_word: bool, +} + +impl TerminalElement { + pub fn new( + terminal: WeakModel, + focused: bool, + cursor_visible: bool, + can_navigate_to_selected_word: bool, + ) -> TerminalElement { + TerminalElement { + terminal, + focused, + cursor_visible, + can_navigate_to_selected_word, + } + } + + //Vec> -> Clip out the parts of the ranges + + fn layout_grid( + grid: &Vec, + text_style: &TextStyle, + // terminal_theme: &TerminalStyle, + text_system: &TextSystem, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + cx: &mut WindowContext<'_>, + ) -> (Vec, Vec) { + let theme_colors = cx.theme().colors(); + let mut cells = vec![]; + let mut rects = vec![]; + + let mut cur_rect: Option = None; + let mut cur_alac_color = None; + + let linegroups = grid.into_iter().group_by(|i| i.point.line); + for (line_index, (_, line)) in linegroups.into_iter().enumerate() { + for cell in line { + let mut fg = cell.fg; + let mut bg = cell.bg; + if cell.flags.contains(Flags::INVERSE) { + mem::swap(&mut fg, &mut bg); + } + + //Expand background rect range + { + if matches!(bg, Named(NamedColor::Background)) { + //Continue to next cell, resetting variables if necessary + cur_alac_color = None; + if let Some(rect) = cur_rect { + rects.push(rect); + cur_rect = None + } + } else { + match cur_alac_color { + Some(cur_color) => { + if bg == cur_color { + cur_rect = cur_rect.take().map(|rect| rect.extend()); + } else { + cur_alac_color = Some(bg); + if cur_rect.is_some() { + rects.push(cur_rect.take().unwrap()); + } + cur_rect = Some(LayoutRect::new( + AlacPoint::new( + line_index as i32, + cell.point.column.0 as i32, + ), + 1, + convert_color(&bg, theme_colors), + )); + } + } + None => { + cur_alac_color = Some(bg); + cur_rect = Some(LayoutRect::new( + AlacPoint::new(line_index as i32, cell.point.column.0 as i32), + 1, + convert_color(&bg, &theme_colors), + )); + } + } + } + } + + //Layout current cell text + { + let cell_text = cell.c.to_string(); + if !is_blank(&cell) { + let cell_style = TerminalElement::cell_style( + &cell, + fg, + bg, + theme_colors, + text_style, + text_system, + hyperlink, + ); + + let layout_cell = text_system + .shape_line( + cell_text.into(), + text_style.font_size.to_pixels(cx.rem_size()), + &[cell_style], + ) + //todo!() Can we remove this unwrap? + .unwrap(); + + cells.push(LayoutCell::new( + AlacPoint::new(line_index as i32, cell.point.column.0 as i32), + layout_cell, + )) + }; + } + } + + if cur_rect.is_some() { + rects.push(cur_rect.take().unwrap()); + } + } + (cells, rects) + } + + // Compute the cursor position and expected block width, may return a zero width if x_for_index returns + // the same position for sequential indexes. Use em_width instead + fn shape_cursor( + cursor_point: DisplayCursor, + size: TerminalSize, + text_fragment: &ShapedLine, + ) -> Option<(Point, Pixels)> { + if cursor_point.line() < size.total_lines() as i32 { + let cursor_width = if text_fragment.width == Pixels::ZERO { + size.cell_width() + } else { + text_fragment.width + }; + + //Cursor should always surround as much of the text as possible, + //hence when on pixel boundaries round the origin down and the width up + Some(( + point( + (cursor_point.col() as f32 * size.cell_width()).floor(), + (cursor_point.line() as f32 * size.line_height()).floor(), + ), + cursor_width.ceil(), + )) + } else { + None + } + } + + ///Convert the Alacritty cell styles to GPUI text styles and background color + fn cell_style( + indexed: &IndexedCell, + fg: terminal::alacritty_terminal::ansi::Color, + bg: terminal::alacritty_terminal::ansi::Color, + colors: &ThemeColors, + text_style: &TextStyle, + text_system: &TextSystem, + hyperlink: Option<(HighlightStyle, &RangeInclusive)>, + ) -> TextRun { + let flags = indexed.cell.flags; + let fg = convert_color(&fg, &colors); + let bg = convert_color(&bg, &colors); + + let mut underline = (flags.intersects(Flags::ALL_UNDERLINES) + || indexed.cell.hyperlink().is_some()) + .then(|| UnderlineStyle { + color: Some(fg), + thickness: Pixels::from(1.0), + wavy: flags.contains(Flags::UNDERCURL), + }); + + //todo!(support bold and italic) + // let mut properties = Properties::new(); + // if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { + // properties = *properties.weight(FontWeight::BOLD); + // } + // if indexed.flags.intersects(Flags::ITALIC) { + // properties = *properties.style(FontStyle::Italic); + // } + + let mut result = TextRun { + len: indexed.c.len_utf8() as usize, + color: fg, + background_color: Some(bg), + font: text_style.font(), + underline, + }; + + if let Some((style, range)) = hyperlink { + if range.contains(&indexed.point) { + if let Some(underline) = style.underline { + result.underline = Some(underline); + } + + if let Some(color) = style.color { + result.color = color; + } + } + } + + result + } + + fn compute_layout(&self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { + let settings = ThemeSettings::get_global(cx); + let terminal_settings = TerminalSettings::get_global(cx); + + //Setup layout information + // todo!(Terminal tooltips) + // let link_style = settings.theme.editor.link_definition; + // let tooltip_style = settings.theme.tooltip.clone(); + + let text_system = cx.text_system(); + let font_size = font_size(&terminal_settings, cx).unwrap_or(settings.buffer_font_size(cx)); + let font_family: SharedString = terminal_settings + .font_family + .as_ref() + .map(|string| SharedString::from(*string)) + .unwrap_or(settings.buffer_font.family); + + let font_features = terminal_settings + .font_features + .as_ref() + .unwrap_or(&settings.buffer_font.features); + + let settings = ThemeSettings::get_global(cx); + let theme = cx.theme(); + let text_style = TextStyle { + font_family, + font_features: *font_features, + font_size: font_size.into(), + font_style: FontStyle::Normal, + line_height: terminal_settings.line_height.value().into(), + background_color: None, + white_space: WhiteSpace::Normal, + // These are going to be overridden per-cell + underline: None, + color: theme.colors().text, + font_weight: FontWeight::NORMAL, + }; + + let selection_color = theme.players().local(); + let match_color = theme.colors().search_match_background; + let gutter; + let dimensions = { + let rem_size = cx.rem_size(); + let font_pixels = text_style.font_size.to_pixels(rem_size); + let line_height = + font_pixels * terminal_settings.line_height.value().to_pixels(rem_size); + let font_id = cx.text_system().font_id(&text_style.font()).unwrap(); + + // todo!(do we need to keep this unwrap?) + let cell_width = text_system + .advance(font_id, font_pixels, 'm') + .unwrap() + .width; + gutter = cell_width; + + let mut size = bounds.size.clone(); + size.width -= gutter; + + TerminalSize::new(line_height, cell_width, size) + }; + + let search_matches = if let Some(terminal_model) = self.terminal.upgrade() { + terminal_model.read(cx).matches.clone() + } else { + Default::default() + }; + + let background_color = theme.colors().background; + let terminal_handle = self.terminal.upgrade().unwrap(); + + let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { + terminal.set_size(dimensions); + terminal.try_sync(cx); + // if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { + // terminal.last_content.last_hovered_word.clone() + // } else { + None + // } + }); + + // let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { + // let mut tooltip = Overlay::new( + // Empty::new() + // .contained() + // .constrained() + // .with_width(dimensions.width()) + // .with_height(dimensions.height()) + // .with_tooltip::( + // hovered_word.id, + // hovered_word.word, + // None, + // tooltip_style, + // cx, + // ), + // ) + // .with_position_mode(gpui::OverlayPositionMode::Local) + // .into_any(); + + // tooltip.layout( + // SizeConstraint::new(Point::zero(), cx.window_size()), + // view_state, + // cx, + // ); + // tooltip + // }); + + let TerminalContent { + cells, + mode, + display_offset, + cursor_char, + selection, + cursor, + .. + } = { &terminal_handle.read(cx).last_content }; + + // searches, highlights to a single range representations + let mut relative_highlighted_ranges = Vec::new(); + for search_match in search_matches { + relative_highlighted_ranges.push((search_match, match_color)) + } + if let Some(selection) = selection { + relative_highlighted_ranges + .push((selection.start..=selection.end, selection_color.cursor)); + } + + // then have that representation be converted to the appropriate highlight data structure + + let (cells, rects) = TerminalElement::layout_grid( + cells, + &text_style, + // &terminal_theme, + &cx.text_system(), + // todo!(Terminal tooltips) + last_hovered_word, + // .as_ref() + // .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), + cx, + ); + + //Layout cursor. Rectangle is used for IME, so we should lay it out even + //if we don't end up showing it. + let cursor = if let AlacCursorShape::Hidden = cursor.shape { + None + } else { + let cursor_point = DisplayCursor::from(cursor.point, *display_offset); + let cursor_text = { + let str_trxt = cursor_char.to_string(); + + let color = if self.focused { + theme.players().local().background + } else { + theme.players().local().cursor + }; + + cx.text_system() + .layout_line( + &str_trxt, + text_style.font_size.to_pixels(cx.rem_size()), + &[TextRun { + len: str_trxt.len(), + font: text_style.font(), + color, + background_color: None, + underline: Default::default(), + }], + ) + //todo!(do we need to keep this unwrap?) + .unwrap() + }; + + let focused = self.focused; + TerminalElement::shape_cursor(cursor_point, dimensions, &cursor_text).map( + move |(cursor_position, block_width)| { + let (shape, text) = match cursor.shape { + AlacCursorShape::Block if !focused => (CursorShape::Hollow, None), + AlacCursorShape::Block => (CursorShape::Block, Some(cursor_text)), + AlacCursorShape::Underline => (CursorShape::Underscore, None), + AlacCursorShape::Beam => (CursorShape::Bar, None), + AlacCursorShape::HollowBlock => (CursorShape::Hollow, None), + //This case is handled in the if wrapping the whole cursor layout + AlacCursorShape::Hidden => unreachable!(), + }; + + Cursor::new( + cursor_position, + block_width, + dimensions.line_height, + terminal_theme.cursor, + shape, + text, + ) + }, + ) + }; + + //Done! + LayoutState { + cells, + cursor, + background_color, + size: dimensions, + rects, + relative_highlighted_ranges, + mode: *mode, + display_offset: *display_offset, + hyperlink_tooltip: None, // todo!(tooltips) + gutter, + } + } + + // todo!() + // fn generic_button_handler( + // connection: WeakModel, + // origin: Point, + // f: impl Fn(&mut Terminal, Point, E, &mut ModelContext), + // ) -> impl Fn(E, &mut TerminalView, &mut EventContext) { + // move |event, _: &mut TerminalView, cx| { + // cx.focus_parent(); + // if let Some(conn_handle) = connection.upgrade() { + // conn_handle.update(cx, |terminal, cx| { + // f(terminal, origin, event, cx); + + // cx.notify(); + // }) + // } + // } + // } + + fn attach_mouse_handlers( + &self, + origin: Point, + visible_bounds: Bounds, + mode: TermMode, + cx: &mut ViewContext, + ) { + // todo!() + // let connection = self.terminal; + + // let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); + + // // Terminal Emulator controlled behavior: + // region = region + // // Start selections + // .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { + // let terminal_view = cx.handle(); + // cx.focus(&terminal_view); + // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); + // if let Some(conn_handle) = connection.upgrade() { + // conn_handle.update(cx, |terminal, cx| { + // terminal.mouse_down(&event, origin); + + // cx.notify(); + // }) + // } + // }) + // // Update drag selections + // .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { + // if event.end { + // return; + // } + + // if cx.is_self_focused() { + // if let Some(conn_handle) = connection.upgrade() { + // conn_handle.update(cx, |terminal, cx| { + // terminal.mouse_drag(event, origin); + // cx.notify(); + // }) + // } + // } + // }) + // // Copy on up behavior + // .on_up( + // MouseButton::Left, + // TerminalElement::generic_button_handler( + // connection, + // origin, + // move |terminal, origin, e, cx| { + // terminal.mouse_up(&e, origin, cx); + // }, + // ), + // ) + // // Context menu + // .on_click( + // MouseButton::Right, + // move |event, view: &mut TerminalView, cx| { + // let mouse_mode = if let Some(conn_handle) = connection.upgrade() { + // conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift)) + // } else { + // // If we can't get the model handle, probably can't deploy the context menu + // true + // }; + // if !mouse_mode { + // view.deploy_context_menu(event.position, cx); + // } + // }, + // ) + // .on_move(move |event, _: &mut TerminalView, cx| { + // if cx.is_self_focused() { + // if let Some(conn_handle) = connection.upgrade() { + // conn_handle.update(cx, |terminal, cx| { + // terminal.mouse_move(&event, origin); + // cx.notify(); + // }) + // } + // } + // }) + // .on_scroll(move |event, _: &mut TerminalView, cx| { + // if let Some(conn_handle) = connection.upgrade() { + // conn_handle.update(cx, |terminal, cx| { + // terminal.scroll_wheel(event, origin); + // cx.notify(); + // }) + // } + // }); + + // // Mouse mode handlers: + // // All mouse modes need the extra click handlers + // if mode.intersects(TermMode::MOUSE_MODE) { + // region = region + // .on_down( + // MouseButton::Right, + // TerminalElement::generic_button_handler( + // connection, + // origin, + // move |terminal, origin, e, _cx| { + // terminal.mouse_down(&e, origin); + // }, + // ), + // ) + // .on_down( + // MouseButton::Middle, + // TerminalElement::generic_button_handler( + // connection, + // origin, + // move |terminal, origin, e, _cx| { + // terminal.mouse_down(&e, origin); + // }, + // ), + // ) + // .on_up( + // MouseButton::Right, + // TerminalElement::generic_button_handler( + // connection, + // origin, + // move |terminal, origin, e, cx| { + // terminal.mouse_up(&e, origin, cx); + // }, + // ), + // ) + // .on_up( + // MouseButton::Middle, + // TerminalElement::generic_button_handler( + // connection, + // origin, + // move |terminal, origin, e, cx| { + // terminal.mouse_up(&e, origin, cx); + // }, + // ), + // ) + // } + + // cx.scene().push_mouse_region(region); + } +} + +impl Element for TerminalElement { + type State = (); + + fn layout( + &mut self, + element_state: Option, + cx: &mut WindowContext<'_>, + ) -> (LayoutId, Self::State) { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + let layout_id = cx.request_layout(&style, None); + + (layout_id, ()) + } + + fn paint(self, bounds: Bounds, _: &mut Self::State, cx: &mut WindowContext<'_>) { + let layout = self.compute_layout(bounds, cx); + // todo!() + // let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); + + // //Setup element stuff + // let clip_bounds = Some(visible_bounds); + + // cx.paint_layer(clip_bounds, |cx| { + // let origin = bounds.origin + point(element_state.gutter, 0.); + + // // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse + // self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx); + + // cx.scene().push_cursor_region(gpui::CursorRegion { + // bounds, + // style: if element_state.hyperlink_tooltip.is_some() { + // CursorStyle::AlacPointingHand + // } else { + // CursorStyle::IBeam + // }, + // }); + + // cx.paint_layer(clip_bounds, |cx| { + // //Start with a background color + // cx.scene().push_quad(Quad { + // bounds, + // background: Some(element_state.background_color), + // border: Default::default(), + // corner_radii: Default::default(), + // }); + + // for rect in &element_state.rects { + // rect.paint(origin, element_state, view_state, cx); + // } + // }); + + // //Draw Highlighted Backgrounds + // cx.paint_layer(clip_bounds, |cx| { + // for (relative_highlighted_range, color) in + // element_state.relative_highlighted_ranges.iter() + // { + // if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines( + // relative_highlighted_range, + // element_state, + // origin, + // ) { + // let hr = HighlightedRange { + // start_y, //Need to change this + // line_height: element_state.size.line_height, + // lines: highlighted_range_lines, + // color: color.clone(), + // //Copied from editor. TODO: move to theme or something + // corner_radius: 0.15 * element_state.size.line_height, + // }; + // hr.paint(bounds, cx); + // } + // } + // }); + + // //Draw the text cells + // cx.paint_layer(clip_bounds, |cx| { + // for cell in &element_state.cells { + // cell.paint(origin, element_state, visible_bounds, view_state, cx); + // } + // }); + + // //Draw cursor + // if self.cursor_visible { + // if let Some(cursor) = &element_state.cursor { + // cx.paint_layer(clip_bounds, |cx| { + // cursor.paint(origin, cx); + // }) + // } + // } + + // if let Some(element) = &mut element_state.hyperlink_tooltip { + // element.paint(origin, visible_bounds, view_state, cx) + // } + // }); + } + + // todo!() remove? + // fn metadata(&self) -> Option<&dyn std::any::Any> { + // None + // } + + // fn debug( + // &self, + // _: Bounds, + // _: &Self::State, + // _: &Self::PaintState, + // _: &TerminalView, + // _: &gpui::ViewContext, + // ) -> gpui::serde_json::Value { + // json!({ + // "type": "TerminalElement", + // }) + // } + + // fn rect_for_text_range( + // &self, + // _: Range, + // bounds: Bounds, + // _: Bounds, + // layout: &Self::State, + // _: &Self::PaintState, + // _: &TerminalView, + // _: &gpui::ViewContext, + // ) -> Option> { + // // Use the same origin that's passed to `Cursor::paint` in the paint + // // method bove. + // let mut origin = bounds.origin() + point(layout.size.cell_width, 0.); + + // // TODO - Why is it necessary to move downward one line to get correct + // // positioning? I would think that we'd want the same rect that is + // // painted for the cursor. + // origin += point(0., layout.size.line_height); + + // Some(layout.cursor.as_ref()?.bounding_rect(origin)) + // } +} + +impl IntoElement for TerminalElement { + type Element = Self; + + fn element_id(&self) -> Option { + todo!() + } + + fn into_element(self) -> Self::Element { + self + } +} + +fn is_blank(cell: &IndexedCell) -> bool { + if cell.c != ' ' { + return false; + } + + if cell.bg != AnsiColor::Named(NamedColor::Background) { + return false; + } + + if cell.hyperlink().is_some() { + return false; + } + + if cell + .flags + .intersects(Flags::ALL_UNDERLINES | Flags::INVERSE | Flags::STRIKEOUT) + { + return false; + } + + return true; +} + +fn to_highlighted_range_lines( + range: &RangeInclusive, + layout: &LayoutState, + origin: Point, +) -> Option<(Pixels, Vec)> { + // Step 1. Normalize the points to be viewport relative. + // When display_offset = 1, here's how the grid is arranged: + //-2,0 -2,1... + //--- Viewport top + //-1,0 -1,1... + //--------- Terminal Top + // 0,0 0,1... + // 1,0 1,1... + //--- Viewport Bottom + // 2,0 2,1... + //--------- Terminal Bottom + + // Normalize to viewport relative, from terminal relative. + // lines are i32s, which are negative above the top left corner of the terminal + // If the user has scrolled, we use the display_offset to tell us which offset + // of the grid data we should be looking at. But for the rendering step, we don't + // want negatives. We want things relative to the 'viewport' (the area of the grid + // which is currently shown according to the display offset) + let unclamped_start = AlacPoint::new( + range.start().line + layout.display_offset, + range.start().column, + ); + let unclamped_end = + AlacPoint::new(range.end().line + layout.display_offset, range.end().column); + + // Step 2. Clamp range to viewport, and return None if it doesn't overlap + if unclamped_end.line.0 < 0 || unclamped_start.line.0 > layout.size.num_lines() as i32 { + return None; + } + + let clamped_start_line = unclamped_start.line.0.max(0) as usize; + let clamped_end_line = unclamped_end.line.0.min(layout.size.num_lines() as i32) as usize; + //Convert the start of the range to pixels + let start_y = origin.y + clamped_start_line as f32 * layout.size.line_height; + + // Step 3. Expand ranges that cross lines into a collection of single-line ranges. + // (also convert to pixels) + let mut highlighted_range_lines = Vec::new(); + for line in clamped_start_line..=clamped_end_line { + let mut line_start = 0; + let mut line_end = layout.size.columns(); + + if line == clamped_start_line { + line_start = unclamped_start.column.0 as usize; + } + if line == clamped_end_line { + line_end = unclamped_end.column.0 as usize + 1; //+1 for inclusive + } + + highlighted_range_lines.push(HighlightedRangeLine { + start_x: origin.x + line_start as f32 * layout.size.cell_width, + end_x: origin.x + line_end as f32 * layout.size.cell_width, + }); + } + + Some((start_y, highlighted_range_lines)) +} + +fn font_size(terminal_settings: &TerminalSettings, cx: &mut AppContext) -> Option { + terminal_settings + .font_size + .map(|size| theme::adjusted_font_size(size, cx)) +} + +// mappings::colors::convert_color +fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, colors: &ThemeColors) -> Hsla { + todo!() +} diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 4a47bc05366c5c8a9063e1b4f8fd4be560b11195..aa07019301fa469d1765d5d2780b6b08a543dbcc 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -49,6 +49,8 @@ impl ThemeColors { tab_bar_background: neutral().light().step_2(), tab_active_background: neutral().light().step_1(), tab_inactive_background: neutral().light().step_2(), + search_match_background: neutral().light().step_2(), // todo!(this was inserted by Mikayla) + editor_background: neutral().light().step_1(), editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors") editor_subheader_background: neutral().light().step_2(), @@ -121,6 +123,8 @@ impl ThemeColors { tab_bar_background: neutral().dark().step_2(), tab_active_background: neutral().dark().step_1(), tab_inactive_background: neutral().dark().step_2(), + search_match_background: neutral().dark().step_2(), // todo!(this was inserted by Mikayla) + editor_background: neutral().dark().step_1(), editor_gutter_background: neutral().dark().step_1(), editor_subheader_background: neutral().dark().step_3(), diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index 2f663618a686c841ec13c4cc083ae44619bfd19c..839f5e13fe2e0db7ccb9a48eebb2bc9ff57a1ed2 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -75,6 +75,8 @@ pub(crate) fn one_dark() -> Theme { tab_bar_background: bg, tab_inactive_background: bg, tab_active_background: editor, + search_match_background: bg, // todo!(this was inserted by Mikayla) + editor_background: editor, editor_gutter_background: editor, editor_subheader_background: bg, @@ -92,6 +94,7 @@ pub(crate) fn one_dark() -> Theme { 0.2, ), editor_document_highlight_write_background: gpui::red(), + terminal_background: bg, // todo!("Use one colors for terminal") terminal_ansi_black: crate::black().dark().step_12(), diff --git a/crates/theme2/src/styles/colors.rs b/crates/theme2/src/styles/colors.rs index 1d4917ac00b99797c077623e13f2ac97bde87dd8..27d891ce94ed1732cf425c514387bae4fb2bc58c 100644 --- a/crates/theme2/src/styles/colors.rs +++ b/crates/theme2/src/styles/colors.rs @@ -114,6 +114,7 @@ pub struct ThemeColors { pub tab_bar_background: Hsla, pub tab_inactive_background: Hsla, pub tab_active_background: Hsla, + pub search_match_background: Hsla, // pub panel_background: Hsla, // pub pane_focused_border: Hsla, // /// The color of the scrollbar thumb. From 379ba620b215e552e927bb606e1d168aae7e86e0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 15:59:40 -0700 Subject: [PATCH 02/90] Move workspace bindings to workspace context Without this, hitting cmd-n on the context menu in the project browser invokes the workspace::NewFile action instead of the project::NewFile action. We're considering changing the behavior so that bindings with no context can only invoke global actions. Co-Authored-By: Max --- assets/keymaps/default.json | 40 ++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 2a8d19f8829039d759c92e79b6acebe79e55b143..25fafa755e842359fd882b416f5e33e06c42a202 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -17,18 +17,8 @@ "cmd-enter": "menu::SecondaryConfirm", "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", - "cmd-{": "pane::ActivatePrevItem", - "cmd-}": "pane::ActivateNextItem", - "alt-cmd-left": "pane::ActivatePrevItem", - "alt-cmd-right": "pane::ActivateNextItem", - "cmd-w": "pane::CloseActiveItem", - "alt-cmd-t": "pane::CloseInactiveItems", - "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", - "cmd-k u": "pane::CloseCleanItems", - "cmd-k cmd-w": "pane::CloseAllItems", "cmd-shift-w": "workspace::CloseWindow", - "cmd-s": "workspace::Save", - "cmd-shift-s": "workspace::SaveAs", + "cmd-o": "workspace::Open", "cmd-=": "zed::IncreaseBufferFontSize", "cmd-+": "zed::IncreaseBufferFontSize", "cmd--": "zed::DecreaseBufferFontSize", @@ -38,15 +28,7 @@ "cmd-h": "zed::Hide", "alt-cmd-h": "zed::HideOthers", "cmd-m": "zed::Minimize", - "ctrl-cmd-f": "zed::ToggleFullScreen", - "cmd-n": "workspace::NewFile", - "cmd-shift-n": "workspace::NewWindow", - "cmd-o": "workspace::Open", - "alt-cmd-o": "projects::OpenRecent", - "alt-cmd-b": "branches::OpenRecent", - "ctrl-~": "workspace::NewTerminal", - "ctrl-`": "terminal_panel::ToggleFocus", - "shift-escape": "workspace::ToggleZoom" + "ctrl-cmd-f": "zed::ToggleFullScreen" } }, { @@ -284,6 +266,15 @@ { "context": "Pane", "bindings": { + "cmd-{": "pane::ActivatePrevItem", + "cmd-}": "pane::ActivateNextItem", + "alt-cmd-left": "pane::ActivatePrevItem", + "alt-cmd-right": "pane::ActivateNextItem", + "cmd-w": "pane::CloseActiveItem", + "alt-cmd-t": "pane::CloseInactiveItems", + "ctrl-alt-cmd-w": "workspace::CloseInactiveTabsAndPanes", + "cmd-k u": "pane::CloseCleanItems", + "cmd-k cmd-w": "pane::CloseAllItems", "cmd-f": "project_search::ToggleFocus", "cmd-g": "search::SelectNextMatch", "cmd-shift-g": "search::SelectPrevMatch", @@ -389,6 +380,15 @@ { "context": "Workspace", "bindings": { + "alt-cmd-o": "projects::OpenRecent", + "alt-cmd-b": "branches::OpenRecent", + "ctrl-~": "workspace::NewTerminal", + "cmd-s": "workspace::Save", + "cmd-shift-s": "workspace::SaveAs", + "cmd-n": "workspace::NewFile", + "cmd-shift-n": "workspace::NewWindow", + "ctrl-`": "terminal_panel::ToggleFocus", + "shift-escape": "workspace::ToggleZoom", "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], "cmd-3": ["workspace::ActivatePane", 2], From 6f7995c150a1f3c722d700e47813899a6c7ccb45 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 16:08:57 -0700 Subject: [PATCH 03/90] Enable workspace::Open global action --- crates/workspace2/src/workspace2.rs | 42 +++++++++++++++-------------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5dcec2cabd5392a5d65695602a1ae6c05aabe63c..40b222d3896b44f5ff674249f56c893b270ea144 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -209,27 +209,29 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &Open, cx: &mut AppContext| { - // let mut paths = cx.prompt_for_paths(PathPromptOptions { - // files: true, - // directories: true, - // multiple: true, - // }); - // if let Some(app_state) = app_state.upgrade() { - // cx.spawn(move |mut cx| async move { - // if let Some(paths) = paths.recv().await.flatten() { - // cx.update(|cx| { - // open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) - // }); - // } - // }) - // .detach(); - // } - // } - // }); + cx.on_action({ + let app_state = Arc::downgrade(&app_state); + move |_: &Open, cx: &mut AppContext| { + let mut paths = cx.prompt_for_paths(PathPromptOptions { + files: true, + directories: true, + multiple: true, + }); + + if let Some(app_state) = app_state.upgrade() { + cx.spawn(move |mut cx| async move { + if let Some(paths) = paths.await.log_err().flatten() { + cx.update(|cx| { + open_paths(&paths, &app_state, None, cx).detach_and_log_err(cx) + }) + .ok(); + } + }) + .detach(); + } + } + }); } type ProjectItemBuilders = From 0edd89a92f4f15bb89fad34e8c054aab9ff07a85 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Fri, 1 Dec 2023 16:17:48 -0700 Subject: [PATCH 04/90] Bind CloseWindow and Open actions on workspace Co-Authored-By: Max --- crates/workspace2/src/workspace2.rs | 171 ++++++++++++++-------------- crates/zed2/src/zed2.rs | 4 +- 2 files changed, 86 insertions(+), 89 deletions(-) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 40b222d3896b44f5ff674249f56c893b270ea144..c063a02174b10ce47fa80cfe09e39b7e5757490b 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -210,6 +210,9 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); notifications::init(cx); + cx.on_action(Workspace::close_global); + cx.on_action(restart); + cx.on_action({ let app_state = Arc::downgrade(&app_state); move |_: &Open, cx: &mut AppContext| { @@ -1178,7 +1181,6 @@ impl Workspace { } } - // todo!(Non-window-actions) pub fn close_global(_: &CloseWindow, cx: &mut AppContext) { cx.windows().iter().find(|window| { window @@ -1196,21 +1198,18 @@ impl Workspace { }); } - pub fn close( - &mut self, - _: &CloseWindow, - cx: &mut ViewContext, - ) -> Option>> { + pub fn close_window(&mut self, _: &CloseWindow, cx: &mut ViewContext) { let window = cx.window_handle(); let prepare = self.prepare_to_close(false, cx); - Some(cx.spawn(|_, mut cx| async move { + cx.spawn(|_, mut cx| async move { if prepare.await? { window.update(&mut cx, |_, cx| { cx.remove_window(); })?; } - Ok(()) - })) + anyhow::Ok(()) + }) + .detach_and_log_err(cx) } pub fn prepare_to_close( @@ -2427,90 +2426,92 @@ impl Workspace { // })) // } - // pub fn follow_next_collaborator( - // &mut self, - // _: &FollowNextCollaborator, - // cx: &mut ViewContext, - // ) -> Option>> { - // let collaborators = self.project.read(cx).collaborators(); - // let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { - // let mut collaborators = collaborators.keys().copied(); - // for peer_id in collaborators.by_ref() { - // if peer_id == leader_id { - // break; - // } - // } - // collaborators.next() - // } else if let Some(last_leader_id) = - // self.last_leaders_by_pane.get(&self.active_pane.downgrade()) - // { - // if collaborators.contains_key(last_leader_id) { - // Some(*last_leader_id) - // } else { - // None + // pub fn follow_next_collaborator( + // &mut self, + // _: &FollowNextCollaborator, + // cx: &mut ViewContext, + // ) { + // let collaborators = self.project.read(cx).collaborators(); + // let next_leader_id = if let Some(leader_id) = self.leader_for_pane(&self.active_pane) { + // let mut collaborators = collaborators.keys().copied(); + // for peer_id in collaborators.by_ref() { + // if peer_id == leader_id { + // break; // } + // } + // collaborators.next() + // } else if let Some(last_leader_id) = + // self.last_leaders_by_pane.get(&self.active_pane.downgrade()) + // { + // if collaborators.contains_key(last_leader_id) { + // Some(*last_leader_id) // } else { // None - // }; - - // let pane = self.active_pane.clone(); - // let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) - // else { - // return None; - // }; - // if Some(leader_id) == self.unfollow(&pane, cx) { - // return None; // } - // self.follow(leader_id, cx) + // } else { + // None + // }; + + // let pane = self.active_pane.clone(); + // let Some(leader_id) = next_leader_id.or_else(|| collaborators.keys().copied().next()) + // else { + // return; + // }; + // if Some(leader_id) == self.unfollow(&pane, cx) { + // return; // } + // if let Some(task) = self.follow(leader_id, cx) { + // task.detach(); + // } + // } - // pub fn follow( - // &mut self, - // leader_id: PeerId, - // cx: &mut ViewContext, - // ) -> Option>> { - // let room = ActiveCall::global(cx).read(cx).room()?.read(cx); - // let project = self.project.read(cx); - - // let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { - // return None; - // }; - - // let other_project_id = match remote_participant.location { - // call::ParticipantLocation::External => None, - // call::ParticipantLocation::UnsharedProject => None, - // call::ParticipantLocation::SharedProject { project_id } => { - // if Some(project_id) == project.remote_id() { - // None - // } else { - // Some(project_id) - // } + // pub fn follow( + // &mut self, + // leader_id: PeerId, + // cx: &mut ViewContext, + // ) -> Option>> { + // let room = ActiveCall::global(cx).read(cx).room()?.read(cx); + // let project = self.project.read(cx); + + // let Some(remote_participant) = room.remote_participant_for_peer_id(leader_id) else { + // return None; + // }; + + // let other_project_id = match remote_participant.location { + // call::ParticipantLocation::External => None, + // call::ParticipantLocation::UnsharedProject => None, + // call::ParticipantLocation::SharedProject { project_id } => { + // if Some(project_id) == project.remote_id() { + // None + // } else { + // Some(project_id) // } - // }; - - // // if they are active in another project, follow there. - // if let Some(project_id) = other_project_id { - // let app_state = self.app_state.clone(); - // return Some(crate::join_remote_project( - // project_id, - // remote_participant.user.id, - // app_state, - // cx, - // )); // } + // }; - // // if you're already following, find the right pane and focus it. - // for (pane, state) in &self.follower_states { - // if leader_id == state.leader_id { - // cx.focus(pane); - // return None; - // } - // } + // // if they are active in another project, follow there. + // if let Some(project_id) = other_project_id { + // let app_state = self.app_state.clone(); + // return Some(crate::join_remote_project( + // project_id, + // remote_participant.user.id, + // app_state, + // cx, + // )); + // } - // // Otherwise, follow. - // self.start_following(leader_id, cx) + // // if you're already following, find the right pane and focus it. + // for (pane, state) in &self.follower_states { + // if leader_id == state.leader_id { + // cx.focus(pane); + // return None; + // } // } + // // Otherwise, follow. + // self.start_following(leader_id, cx) + // } + pub fn unfollow(&mut self, pane: &View, cx: &mut ViewContext) -> Option { let follower_states = &mut self.follower_states; let state = follower_states.remove(pane)?; @@ -3287,13 +3288,8 @@ impl Workspace { fn actions(&self, div: Div, cx: &mut ViewContext) -> Div { self.add_workspace_actions_listeners(div, cx) - // cx.add_async_action(Workspace::open); - // cx.add_async_action(Workspace::follow_next_collaborator); - // cx.add_async_action(Workspace::close); .on_action(cx.listener(Self::close_inactive_items_and_panes)) .on_action(cx.listener(Self::close_all_items_and_panes)) - // cx.add_global_action(Workspace::close_global); - // cx.add_global_action(restart); .on_action(cx.listener(Self::save_all)) .on_action(cx.listener(Self::add_folder_to_project)) .on_action(cx.listener(|workspace, _: &Unfollow, cx| { @@ -3342,6 +3338,9 @@ impl Workspace { workspace.close_all_docks(cx); }), ) + .on_action(cx.listener(Workspace::open)) + .on_action(cx.listener(Workspace::close_window)) + // cx.add_action(Workspace::activate_pane_at_index); // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { // workspace.reopen_closed_item(cx).detach(); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index b8976874898cc53f59a98fffeb6113e69afe7df5..8ff0d2a61976995c4ce8faad215ddf401f02c28e 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -168,9 +168,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.on_window_should_close(move |cx| { handle .update(cx, |workspace, cx| { - if let Some(task) = workspace.close(&Default::default(), cx) { - task.detach_and_log_err(cx); - } + workspace.close_window(&Default::default(), cx); false }) .unwrap_or(true) From b0f91441679bfec0faa2101f4dacbeea7aea97bc Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 1 Dec 2023 16:21:12 -0800 Subject: [PATCH 05/90] Compiling layout! co-authored-by: Max --- crates/terminal_view2/src/terminal_element.rs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 5c4635bac5d238e143153f8a15d22709d8c2170c..c2be4f62afcd3ff6463b5b998ba4c8f4c3dcdab0 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -182,7 +182,7 @@ impl TerminalElement { // terminal_theme: &TerminalStyle, text_system: &TextSystem, hyperlink: Option<(HighlightStyle, &RangeInclusive)>, - cx: &mut WindowContext<'_>, + cx: &WindowContext<'_>, ) -> (Vec, Vec) { let theme_colors = cx.theme().colors(); let mut cells = vec![]; @@ -362,35 +362,41 @@ impl TerminalElement { } fn compute_layout(&self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { - let settings = ThemeSettings::get_global(cx); - let terminal_settings = TerminalSettings::get_global(cx); + let settings = ThemeSettings::get_global(cx).clone(); //Setup layout information // todo!(Terminal tooltips) // let link_style = settings.theme.editor.link_definition; // let tooltip_style = settings.theme.tooltip.clone(); - let text_system = cx.text_system(); - let font_size = font_size(&terminal_settings, cx).unwrap_or(settings.buffer_font_size(cx)); - let font_family: SharedString = terminal_settings + let buffer_font_size = settings.buffer_font_size(cx); + + let terminal_settings = TerminalSettings::get_global(cx); + let font_family = terminal_settings .font_family .as_ref() - .map(|string| SharedString::from(*string)) + .map(|string| string.clone().into()) .unwrap_or(settings.buffer_font.family); let font_features = terminal_settings .font_features - .as_ref() - .unwrap_or(&settings.buffer_font.features); + .clone() + .unwrap_or(settings.buffer_font.features.clone()); + + let line_height = terminal_settings.line_height.value(); + let font_size = terminal_settings.font_size.clone(); + + let font_size = + font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); let settings = ThemeSettings::get_global(cx); - let theme = cx.theme(); + let theme = cx.theme().clone(); let text_style = TextStyle { font_family, - font_features: *font_features, + font_features, font_size: font_size.into(), font_style: FontStyle::Normal, - line_height: terminal_settings.line_height.value().into(), + line_height: line_height.into(), background_color: None, white_space: WhiteSpace::Normal, // These are going to be overridden per-cell @@ -399,14 +405,14 @@ impl TerminalElement { font_weight: FontWeight::NORMAL, }; + let text_system = cx.text_system(); let selection_color = theme.players().local(); let match_color = theme.colors().search_match_background; let gutter; let dimensions = { let rem_size = cx.rem_size(); let font_pixels = text_style.font_size.to_pixels(rem_size); - let line_height = - font_pixels * terminal_settings.line_height.value().to_pixels(rem_size); + let line_height = font_pixels * line_height.to_pixels(rem_size); let font_id = cx.text_system().font_id(&text_style.font()).unwrap(); // todo!(do we need to keep this unwrap?) @@ -475,7 +481,7 @@ impl TerminalElement { selection, cursor, .. - } = { &terminal_handle.read(cx).last_content }; + } = &terminal_handle.read(cx).last_content; // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); @@ -516,12 +522,13 @@ impl TerminalElement { theme.players().local().cursor }; + let len = str_trxt.len(); cx.text_system() - .layout_line( - &str_trxt, + .shape_line( + str_trxt.into(), text_style.font_size.to_pixels(cx.rem_size()), &[TextRun { - len: str_trxt.len(), + len, font: text_style.font(), color, background_color: None, @@ -549,7 +556,7 @@ impl TerminalElement { cursor_position, block_width, dimensions.line_height, - terminal_theme.cursor, + theme.players().local().cursor, shape, text, ) @@ -964,12 +971,6 @@ fn to_highlighted_range_lines( Some((start_y, highlighted_range_lines)) } -fn font_size(terminal_settings: &TerminalSettings, cx: &mut AppContext) -> Option { - terminal_settings - .font_size - .map(|size| theme::adjusted_font_size(size, cx)) -} - // mappings::colors::convert_color fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, colors: &ThemeColors) -> Hsla { todo!() From fe839197464361b5aefa168df849c572e4deb56a Mon Sep 17 00:00:00 2001 From: Mikayla Date: Fri, 1 Dec 2023 17:01:57 -0800 Subject: [PATCH 06/90] Get terminal turning on --- crates/client/src/telemetry.rs | 1 - crates/terminal2/src/mappings/colors.rs | 125 ------- crates/terminal2/src/terminal2.rs | 1 + crates/terminal_view2/src/terminal_element.rs | 318 ++++++++++++------ crates/terminal_view2/src/terminal_view.rs | 19 +- 5 files changed, 223 insertions(+), 241 deletions(-) diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index e2fc8ad3adeebe36b62962a20fa98d027e9ebe0b..a3e7449cf8a4be5a25b4f7eaaa4f3d2379d70d68 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -350,7 +350,6 @@ impl Telemetry { milliseconds_since_first_event: self.milliseconds_since_first_event(), }; - dbg!(telemetry_settings); self.report_clickhouse_event(event, telemetry_settings, true) } diff --git a/crates/terminal2/src/mappings/colors.rs b/crates/terminal2/src/mappings/colors.rs index d3c8443cbf962c68dc3dd124aff2495d0a88ddc0..adcc0d8080c9754c327ce80e4d3f56b1c6405b54 100644 --- a/crates/terminal2/src/mappings/colors.rs +++ b/crates/terminal2/src/mappings/colors.rs @@ -1,118 +1,5 @@ -// todo!() use alacritty_terminal::term::color::Rgb as AlacRgb; -// use gpui::color::Color; -// use theme2::TerminalStyle; -///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent -// pub fn convert_color(alac_color: &AnsiColor, style: &TerminalStyle) -> Color { -// match alac_color { -// //Named and theme defined colors -// alacritty_terminal::ansi::Color::Named(n) => match n { -// alacritty_terminal::ansi::NamedColor::Black => style.black, -// alacritty_terminal::ansi::NamedColor::Red => style.red, -// alacritty_terminal::ansi::NamedColor::Green => style.green, -// alacritty_terminal::ansi::NamedColor::Yellow => style.yellow, -// alacritty_terminal::ansi::NamedColor::Blue => style.blue, -// alacritty_terminal::ansi::NamedColor::Magenta => style.magenta, -// alacritty_terminal::ansi::NamedColor::Cyan => style.cyan, -// alacritty_terminal::ansi::NamedColor::White => style.white, -// alacritty_terminal::ansi::NamedColor::BrightBlack => style.bright_black, -// alacritty_terminal::ansi::NamedColor::BrightRed => style.bright_red, -// alacritty_terminal::ansi::NamedColor::BrightGreen => style.bright_green, -// alacritty_terminal::ansi::NamedColor::BrightYellow => style.bright_yellow, -// alacritty_terminal::ansi::NamedColor::BrightBlue => style.bright_blue, -// alacritty_terminal::ansi::NamedColor::BrightMagenta => style.bright_magenta, -// alacritty_terminal::ansi::NamedColor::BrightCyan => style.bright_cyan, -// alacritty_terminal::ansi::NamedColor::BrightWhite => style.bright_white, -// alacritty_terminal::ansi::NamedColor::Foreground => style.foreground, -// alacritty_terminal::ansi::NamedColor::Background => style.background, -// alacritty_terminal::ansi::NamedColor::Cursor => style.cursor, -// alacritty_terminal::ansi::NamedColor::DimBlack => style.dim_black, -// alacritty_terminal::ansi::NamedColor::DimRed => style.dim_red, -// alacritty_terminal::ansi::NamedColor::DimGreen => style.dim_green, -// alacritty_terminal::ansi::NamedColor::DimYellow => style.dim_yellow, -// alacritty_terminal::ansi::NamedColor::DimBlue => style.dim_blue, -// alacritty_terminal::ansi::NamedColor::DimMagenta => style.dim_magenta, -// alacritty_terminal::ansi::NamedColor::DimCyan => style.dim_cyan, -// alacritty_terminal::ansi::NamedColor::DimWhite => style.dim_white, -// alacritty_terminal::ansi::NamedColor::BrightForeground => style.bright_foreground, -// alacritty_terminal::ansi::NamedColor::DimForeground => style.dim_foreground, -// }, -// //'True' colors -// alacritty_terminal::ansi::Color::Spec(rgb) => Color::new(rgb.r, rgb.g, rgb.b, u8::MAX), -// //8 bit, indexed colors -// alacritty_terminal::ansi::Color::Indexed(i) => get_color_at_index(&(*i as usize), style), -// } -// } - -/// TODO: Move this -///Converts an 8 bit ANSI color to it's GPUI equivalent. -///Accepts usize for compatibility with the alacritty::Colors interface, -///Other than that use case, should only be called with values in the [0,255] range -// pub fn get_color_at_index(index: &usize, style: &TerminalStyle) -> Color { -// match index { -// //0-15 are the same as the named colors above -// 0 => style.black, -// 1 => style.red, -// 2 => style.green, -// 3 => style.yellow, -// 4 => style.blue, -// 5 => style.magenta, -// 6 => style.cyan, -// 7 => style.white, -// 8 => style.bright_black, -// 9 => style.bright_red, -// 10 => style.bright_green, -// 11 => style.bright_yellow, -// 12 => style.bright_blue, -// 13 => style.bright_magenta, -// 14 => style.bright_cyan, -// 15 => style.bright_white, -// //16-231 are mapped to their RGB colors on a 0-5 range per channel -// 16..=231 => { -// let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components -// let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow -// Color::new(r * step, g * step, b * step, u8::MAX) //Map the ANSI-RGB components to an RGB color -// } -// //232-255 are a 24 step grayscale from black to white -// 232..=255 => { -// let i = *index as u8 - 232; //Align index to 0..24 -// let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks -// Color::new(i * step, i * step, i * step, u8::MAX) //Map the ANSI-grayscale components to the RGB-grayscale -// } -// //For compatibility with the alacritty::Colors interface -// 256 => style.foreground, -// 257 => style.background, -// 258 => style.cursor, -// 259 => style.dim_black, -// 260 => style.dim_red, -// 261 => style.dim_green, -// 262 => style.dim_yellow, -// 263 => style.dim_blue, -// 264 => style.dim_magenta, -// 265 => style.dim_cyan, -// 266 => style.dim_white, -// 267 => style.bright_foreground, -// 268 => style.black, //'Dim Background', non-standard color -// _ => Color::new(0, 0, 0, 255), -// } -// } -///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube -///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). -/// -///Wikipedia gives a formula for calculating the index for a given color: -/// -///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) -/// -///This function does the reverse, calculating the r, g, and b components from a given index. -// fn rgb_for_index(i: &u8) -> (u8, u8, u8) { -// debug_assert!((&16..=&231).contains(&i)); -// let i = i - 16; -// let r = (i - (i % 36)) / 36; -// let g = ((i % 36) - (i % 6)) / 6; -// let b = (i % 36) % 6; -// (r, g, b) -// } use gpui::Rgba; //Convenience method to convert from a GPUI color to an alacritty Rgb @@ -123,15 +10,3 @@ pub fn to_alac_rgb(color: impl Into) -> AlacRgb { let b = ((color.b * color.a) * 255.) as u8; AlacRgb::new(r, g, b) } - -// #[cfg(test)] -// mod tests { -// #[test] -// fn test_rgb_for_index() { -// //Test every possible value in the color cube -// for i in 16..=231 { -// let (r, g, b) = crate::mappings::colors::rgb_for_index(&(i as u8)); -// assert_eq!(i, 16 + 36 * r + 6 * g + b); -// } -// } -// } diff --git a/crates/terminal2/src/terminal2.rs b/crates/terminal2/src/terminal2.rs index 9f94339504b7c7b1705161bf9ea876021ec49028..761bee26b2882e4089ad3730b14ed9de256f0c87 100644 --- a/crates/terminal2/src/terminal2.rs +++ b/crates/terminal2/src/terminal2.rs @@ -976,6 +976,7 @@ impl Terminal { } pub fn try_sync(&mut self, cx: &mut ModelContext) { + println!("trying to sync"); let term = self.term.clone(); let mut terminal = if let Some(term) = term.try_lock_unfair() { diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index c2be4f62afcd3ff6463b5b998ba4c8f4c3dcdab0..7b7c2a304140ed2d3e91325e7bb8550281157cd7 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,18 +1,17 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ - point, px, relative, rems, transparent_black, AnyElement, AppContext, Bounds, Component, - CursorStyle, Element, ElementId, FontStyle, FontWeight, HighlightStyle, Hsla, IntoElement, - IsZero, LayoutId, ModelContext, Overlay, Pixels, Point, Quad, ShapedLine, SharedString, Style, - Styled, TextRun, TextStyle, TextSystem, Underline, UnderlineStyle, ViewContext, WeakModel, + black, point, px, red, relative, transparent_black, AnyElement, Bounds, Element, ElementId, + Font, FontStyle, FontWeight, HighlightStyle, Hsla, IntoElement, LayoutId, Pixels, Point, Rgba, + ShapedLine, Style, TextRun, TextStyle, TextSystem, UnderlineStyle, ViewContext, WeakModel, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; -use ordered_float::OrderedFloat; use settings::Settings; use terminal::{ + alacritty_terminal::ansi::NamedColor, alacritty_terminal::{ - ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape, NamedColor}, + ansi::{Color as AnsiColor, Color::Named, CursorShape as AlacCursorShape}, grid::Dimensions, index::Point as AlacPoint, term::{cell::Flags, TermMode}, @@ -20,7 +19,7 @@ use terminal::{ terminal_settings::TerminalSettings, IndexedCell, Terminal, TerminalContent, TerminalSize, }; -use theme::{ActiveTheme, ThemeColors, ThemeSettings}; +use theme::{ActiveTheme, Theme, ThemeSettings}; use std::mem; use std::{fmt::Debug, ops::RangeInclusive}; @@ -80,7 +79,6 @@ impl LayoutCell { origin: Point, layout: &LayoutState, _visible_bounds: Bounds, - _view: &mut TerminalView, cx: &mut WindowContext, ) { let pos = { @@ -92,7 +90,7 @@ impl LayoutCell { ) }; - self.text.paint(pos, layout.size.line_height, cx); + self.text.paint(pos, layout.size.line_height, cx).ok(); } } @@ -120,13 +118,7 @@ impl LayoutRect { } } - fn paint( - &self, - origin: Point, - layout: &LayoutState, - _view: &mut TerminalView, - cx: &mut ViewContext, - ) { + fn paint(&self, origin: Point, layout: &LayoutState, cx: &mut WindowContext) { let position = { let alac_point = self.point; point( @@ -184,7 +176,7 @@ impl TerminalElement { hyperlink: Option<(HighlightStyle, &RangeInclusive)>, cx: &WindowContext<'_>, ) -> (Vec, Vec) { - let theme_colors = cx.theme().colors(); + let theme = cx.theme(); let mut cells = vec![]; let mut rects = vec![]; @@ -225,7 +217,7 @@ impl TerminalElement { cell.point.column.0 as i32, ), 1, - convert_color(&bg, theme_colors), + convert_color(&bg, theme), )); } } @@ -234,7 +226,7 @@ impl TerminalElement { cur_rect = Some(LayoutRect::new( AlacPoint::new(line_index as i32, cell.point.column.0 as i32), 1, - convert_color(&bg, &theme_colors), + convert_color(&bg, &theme), )); } } @@ -249,7 +241,7 @@ impl TerminalElement { &cell, fg, bg, - theme_colors, + theme, text_style, text_system, hyperlink, @@ -312,7 +304,7 @@ impl TerminalElement { indexed: &IndexedCell, fg: terminal::alacritty_terminal::ansi::Color, bg: terminal::alacritty_terminal::ansi::Color, - colors: &ThemeColors, + colors: &Theme, text_style: &TextStyle, text_system: &TextSystem, hyperlink: Option<(HighlightStyle, &RangeInclusive)>, @@ -321,7 +313,7 @@ impl TerminalElement { let fg = convert_color(&fg, &colors); let bg = convert_color(&bg, &colors); - let mut underline = (flags.intersects(Flags::ALL_UNDERLINES) + let underline = (flags.intersects(Flags::ALL_UNDERLINES) || indexed.cell.hyperlink().is_some()) .then(|| UnderlineStyle { color: Some(fg), @@ -329,20 +321,27 @@ impl TerminalElement { wavy: flags.contains(Flags::UNDERCURL), }); - //todo!(support bold and italic) - // let mut properties = Properties::new(); - // if indexed.flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { - // properties = *properties.weight(FontWeight::BOLD); - // } - // if indexed.flags.intersects(Flags::ITALIC) { - // properties = *properties.style(FontStyle::Italic); - // } + let weight = if flags.intersects(Flags::BOLD | Flags::DIM_BOLD) { + FontWeight::BOLD + } else { + FontWeight::NORMAL + }; + + let style = if flags.intersects(Flags::ITALIC) { + FontStyle::Italic + } else { + FontStyle::Normal + }; let mut result = TextRun { len: indexed.c.len_utf8() as usize, color: fg, background_color: Some(bg), - font: text_style.font(), + font: Font { + weight, + style, + ..text_style.font() + }, underline, }; @@ -498,7 +497,6 @@ impl TerminalElement { let (cells, rects) = TerminalElement::layout_grid( cells, &text_style, - // &terminal_theme, &cx.text_system(), // todo!(Terminal tooltips) last_hovered_word, @@ -752,84 +750,54 @@ impl Element for TerminalElement { fn paint(self, bounds: Bounds, _: &mut Self::State, cx: &mut WindowContext<'_>) { let layout = self.compute_layout(bounds, cx); - // todo!() - // let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default(); - - // //Setup element stuff - // let clip_bounds = Some(visible_bounds); - - // cx.paint_layer(clip_bounds, |cx| { - // let origin = bounds.origin + point(element_state.gutter, 0.); - // // Elements are ephemeral, only at paint time do we know what could be clicked by a mouse - // self.attach_mouse_handlers(origin, visible_bounds, element_state.mode, cx); - - // cx.scene().push_cursor_region(gpui::CursorRegion { - // bounds, - // style: if element_state.hyperlink_tooltip.is_some() { - // CursorStyle::AlacPointingHand - // } else { - // CursorStyle::IBeam - // }, - // }); + let theme = cx.theme(); + cx.paint_quad( + bounds, + Default::default(), + theme.colors().editor_background, + Default::default(), + Hsla::default(), + ); + let origin = bounds.origin + Point::new(layout.gutter, px(0.)); - // cx.paint_layer(clip_bounds, |cx| { - // //Start with a background color - // cx.scene().push_quad(Quad { - // bounds, - // background: Some(element_state.background_color), - // border: Default::default(), - // corner_radii: Default::default(), - // }); - - // for rect in &element_state.rects { - // rect.paint(origin, element_state, view_state, cx); - // } - // }); + for rect in &layout.rects { + rect.paint(origin, &layout, cx); + } - // //Draw Highlighted Backgrounds - // cx.paint_layer(clip_bounds, |cx| { - // for (relative_highlighted_range, color) in - // element_state.relative_highlighted_ranges.iter() - // { - // if let Some((start_y, highlighted_range_lines)) = to_highlighted_range_lines( - // relative_highlighted_range, - // element_state, - // origin, - // ) { - // let hr = HighlightedRange { - // start_y, //Need to change this - // line_height: element_state.size.line_height, - // lines: highlighted_range_lines, - // color: color.clone(), - // //Copied from editor. TODO: move to theme or something - // corner_radius: 0.15 * element_state.size.line_height, - // }; - // hr.paint(bounds, cx); - // } - // } - // }); + cx.with_z_index(1, |cx| { + for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() { + if let Some((start_y, highlighted_range_lines)) = + to_highlighted_range_lines(relative_highlighted_range, &layout, origin) + { + let hr = HighlightedRange { + start_y, //Need to change this + line_height: layout.size.line_height, + lines: highlighted_range_lines, + color: color.clone(), + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.size.line_height, + }; + hr.paint(bounds, cx); + } + } + }); - // //Draw the text cells - // cx.paint_layer(clip_bounds, |cx| { - // for cell in &element_state.cells { - // cell.paint(origin, element_state, visible_bounds, view_state, cx); - // } - // }); + cx.with_z_index(2, |cx| { + for cell in &layout.cells { + cell.paint(origin, &layout, bounds, cx); + } + }); - // //Draw cursor - // if self.cursor_visible { - // if let Some(cursor) = &element_state.cursor { - // cx.paint_layer(clip_bounds, |cx| { - // cursor.paint(origin, cx); - // }) - // } - // } + cx.with_z_index(3, |cx| { + if let Some(cursor) = &layout.cursor { + cursor.paint(origin, cx); + } + }); // if let Some(element) = &mut element_state.hyperlink_tooltip { // element.paint(origin, visible_bounds, view_state, cx) // } - // }); } // todo!() remove? @@ -877,7 +845,7 @@ impl IntoElement for TerminalElement { type Element = Self; fn element_id(&self) -> Option { - todo!() + Some("terminal".into()) } fn into_element(self) -> Self::Element { @@ -971,7 +939,147 @@ fn to_highlighted_range_lines( Some((start_y, highlighted_range_lines)) } -// mappings::colors::convert_color -fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, colors: &ThemeColors) -> Hsla { - todo!() +///Converts a 2, 8, or 24 bit color ANSI color to the GPUI equivalent +fn convert_color(fg: &terminal::alacritty_terminal::ansi::Color, theme: &Theme) -> Hsla { + let colors = theme.colors(); + match fg { + //Named and theme defined colors + terminal::alacritty_terminal::ansi::Color::Named(n) => match n { + NamedColor::Black => colors.terminal_ansi_black, + NamedColor::Red => colors.terminal_ansi_red, + NamedColor::Green => colors.terminal_ansi_green, + NamedColor::Yellow => colors.terminal_ansi_yellow, + NamedColor::Blue => colors.terminal_ansi_blue, + NamedColor::Magenta => colors.terminal_ansi_magenta, + NamedColor::Cyan => colors.terminal_ansi_cyan, + NamedColor::White => colors.terminal_ansi_white, + NamedColor::BrightBlack => colors.terminal_ansi_bright_black, + NamedColor::BrightRed => colors.terminal_ansi_bright_red, + NamedColor::BrightGreen => colors.terminal_ansi_bright_green, + NamedColor::BrightYellow => colors.terminal_ansi_bright_yellow, + NamedColor::BrightBlue => colors.terminal_ansi_bright_blue, + NamedColor::BrightMagenta => colors.terminal_ansi_bright_magenta, + NamedColor::BrightCyan => colors.terminal_ansi_bright_cyan, + NamedColor::BrightWhite => colors.terminal_ansi_bright_white, + NamedColor::Foreground => colors.text, + NamedColor::Background => colors.background, + NamedColor::Cursor => theme.players().local().cursor, + + // todo!(more colors) + NamedColor::DimBlack => red(), + NamedColor::DimRed => red(), + NamedColor::DimGreen => red(), + NamedColor::DimYellow => red(), + NamedColor::DimBlue => red(), + NamedColor::DimMagenta => red(), + NamedColor::DimCyan => red(), + NamedColor::DimWhite => red(), + NamedColor::BrightForeground => red(), + NamedColor::DimForeground => red(), + }, + //'True' colors + terminal::alacritty_terminal::ansi::Color::Spec(rgb) => rgba_color(rgb.r, rgb.g, rgb.b), + //8 bit, indexed colors + terminal::alacritty_terminal::ansi::Color::Indexed(i) => { + get_color_at_index(&(*i as usize), theme) + } + } +} + +///Converts an 8 bit ANSI color to it's GPUI equivalent. +///Accepts usize for compatibility with the alacritty::Colors interface, +///Other than that use case, should only be called with values in the [0,255] range +pub fn get_color_at_index(index: &usize, theme: &Theme) -> Hsla { + let colors = theme.colors(); + + match index { + //0-15 are the same as the named colors above + 0 => colors.terminal_ansi_black, + 1 => colors.terminal_ansi_red, + 2 => colors.terminal_ansi_green, + 3 => colors.terminal_ansi_yellow, + 4 => colors.terminal_ansi_blue, + 5 => colors.terminal_ansi_magenta, + 6 => colors.terminal_ansi_cyan, + 7 => colors.terminal_ansi_white, + 8 => colors.terminal_ansi_bright_black, + 9 => colors.terminal_ansi_bright_red, + 10 => colors.terminal_ansi_bright_green, + 11 => colors.terminal_ansi_bright_yellow, + 12 => colors.terminal_ansi_bright_blue, + 13 => colors.terminal_ansi_bright_magenta, + 14 => colors.terminal_ansi_bright_cyan, + 15 => colors.terminal_ansi_bright_white, + //16-231 are mapped to their RGB colors on a 0-5 range per channel + 16..=231 => { + let (r, g, b) = rgb_for_index(&(*index as u8)); //Split the index into it's ANSI-RGB components + let step = (u8::MAX as f32 / 5.).floor() as u8; //Split the RGB range into 5 chunks, with floor so no overflow + rgba_color(r * step, g * step, b * step) //Map the ANSI-RGB components to an RGB color + } + //232-255 are a 24 step grayscale from black to white + 232..=255 => { + let i = *index as u8 - 232; //Align index to 0..24 + let step = (u8::MAX as f32 / 24.).floor() as u8; //Split the RGB grayscale values into 24 chunks + rgba_color(i * step, i * step, i * step) //Map the ANSI-grayscale components to the RGB-grayscale + } + //For compatibility with the alacritty::Colors interface + 256 => colors.text, + 257 => colors.background, + 258 => theme.players().local().cursor, + + // todo!(more colors) + 259 => red(), //style.dim_black, + 260 => red(), //style.dim_red, + 261 => red(), //style.dim_green, + 262 => red(), //style.dim_yellow, + 263 => red(), //style.dim_blue, + 264 => red(), //style.dim_magenta, + 265 => red(), //style.dim_cyan, + 266 => red(), //style.dim_white, + 267 => red(), //style.bright_foreground, + 268 => colors.terminal_ansi_black, //'Dim Background', non-standard color + + _ => black(), + } +} + +///Generates the rgb channels in [0, 5] for a given index into the 6x6x6 ANSI color cube +///See: [8 bit ansi color](https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit). +/// +///Wikipedia gives a formula for calculating the index for a given color: +/// +///index = 16 + 36 × r + 6 × g + b (0 ≤ r, g, b ≤ 5) +/// +///This function does the reverse, calculating the r, g, and b components from a given index. +fn rgb_for_index(i: &u8) -> (u8, u8, u8) { + debug_assert!((&16..=&231).contains(&i)); + let i = i - 16; + let r = (i - (i % 36)) / 36; + let g = ((i % 36) - (i % 6)) / 6; + let b = (i % 36) % 6; + (r, g, b) +} + +fn rgba_color(r: u8, g: u8, b: u8) -> Hsla { + Rgba { + r: (r as f32 / 255.) as f32, + g: (g as f32 / 255.) as f32, + b: (b as f32 / 255.) as f32, + a: 1., + } + .into() +} + +#[cfg(test)] +mod tests { + use crate::terminal_element::rgb_for_index; + + #[test] + fn test_rgb_for_index() { + //Test every possible value in the color cube + for i in 16..=231 { + let (r, g, b) = rgb_for_index(&(i as u8)); + assert_eq!(i, 16 + 36 * r + 6 * g + b); + } + } } diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index b007d58c34bcb2163f42bd2b88e1979a18152f56..62306a8644267dc3528068b45f977f4f922b2af2 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -25,6 +25,7 @@ use terminal::{ terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, Event, MaybeNavigationTarget, Terminal, }; +use terminal_element::TerminalElement; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, @@ -305,7 +306,6 @@ impl TerminalView { cx, ) })); - dbg!(&position); // todo!() // self.context_menu // .show(position, AnchorCorner::TopLeft, menu_entries, cx); @@ -541,11 +541,13 @@ impl Render for TerminalView { let focused = self.focus_handle.is_focused(cx); div() + .size_full() .relative() .child( div() .z_index(0) .absolute() + .size_full() .on_key_down(cx.listener(Self::key_down)) .on_action(cx.listener(TerminalView::send_text)) .on_action(cx.listener(TerminalView::send_keystroke)) @@ -554,15 +556,12 @@ impl Render for TerminalView { .on_action(cx.listener(TerminalView::clear)) .on_action(cx.listener(TerminalView::show_character_palette)) .on_action(cx.listener(TerminalView::select_all)) - // todo!() - .child( - "TERMINAL HERE", // TerminalElement::new( - // terminal_handle, - // focused, - // self.should_show_cursor(focused, cx), - // self.can_navigate_to_selected_word, - // ) - ) + .child(TerminalElement::new( + terminal_handle, + focused, + self.should_show_cursor(focused, cx), + self.can_navigate_to_selected_word, + )) .on_mouse_down( MouseButton::Right, cx.listener(|this, event: &MouseDownEvent, cx| { From 5bdaf0e074f2c034ff418ae713cfaa20be811eab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 17:54:37 -0500 Subject: [PATCH 07/90] Work on light theme, update tab --- crates/theme2/src/default_colors.rs | 24 +++--- crates/theme2/src/default_theme.rs | 78 +++++++++---------- crates/theme2/src/registry.rs | 4 +- crates/theme2/src/styles/syntax.rs | 16 ++-- crates/ui2/src/components/icon.rs | 2 +- crates/workspace2/src/pane.rs | 111 ++++++++++++++++------------ 6 files changed, 125 insertions(+), 110 deletions(-) diff --git a/crates/theme2/src/default_colors.rs b/crates/theme2/src/default_colors.rs index 4a47bc05366c5c8a9063e1b4f8fd4be560b11195..b61e4792a4242199585e3a17bac493bfce7797e3 100644 --- a/crates/theme2/src/default_colors.rs +++ b/crates/theme2/src/default_colors.rs @@ -5,7 +5,7 @@ use crate::ColorScale; use crate::{SystemColors, ThemeColors}; pub(crate) fn neutral() -> ColorScaleSet { - slate() + sand() } impl ThemeColors { @@ -29,12 +29,12 @@ impl ThemeColors { element_disabled: neutral().light_alpha().step_3(), drop_target_background: blue().light_alpha().step_2(), ghost_element_background: system.transparent, - ghost_element_hover: neutral().light_alpha().step_4(), - ghost_element_active: neutral().light_alpha().step_5(), + ghost_element_hover: neutral().light_alpha().step_3(), + ghost_element_active: neutral().light_alpha().step_4(), ghost_element_selected: neutral().light_alpha().step_5(), ghost_element_disabled: neutral().light_alpha().step_3(), - text: yellow().light().step_9(), - text_muted: neutral().light().step_11(), + text: neutral().light().step_12(), + text_muted: neutral().light().step_10(), text_placeholder: neutral().light().step_10(), text_disabled: neutral().light().step_9(), text_accent: blue().light().step_11(), @@ -53,13 +53,13 @@ impl ThemeColors { editor_gutter_background: neutral().light().step_1(), // todo!("pick the right colors") editor_subheader_background: neutral().light().step_2(), editor_active_line_background: neutral().light_alpha().step_3(), - editor_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors") - editor_active_line_number: neutral().light_alpha().step_3(), // todo!("pick the right colors") - editor_highlighted_line_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_invisible: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_active_wrap_guide: neutral().light_alpha().step_4(), // todo!("pick the right colors") - editor_document_highlight_read_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") + editor_line_number: neutral().light().step_10(), + editor_active_line_number: neutral().light().step_11(), + editor_highlighted_line_background: neutral().light_alpha().step_3(), + editor_invisible: neutral().light().step_10(), + editor_wrap_guide: neutral().light_alpha().step_7(), + editor_active_wrap_guide: neutral().light_alpha().step_8(), // todo!("pick the right colors") + editor_document_highlight_read_background: neutral().light_alpha().step_3(), // todo!("pick the right colors") editor_document_highlight_write_background: neutral().light_alpha().step_4(), // todo!("pick the right colors") terminal_background: neutral().light().step_1(), terminal_ansi_black: black().light().step_12(), diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 8502f433f4a919d7d661f00e55d0dd353ff46fc5..269414b36a0747e5e1bfed677bfb69378ec2ab03 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,47 +1,49 @@ +use std::sync::Arc; + use crate::{ one_themes::{one_dark, one_family}, - Theme, ThemeFamily, + Theme, ThemeFamily, Appearance, ThemeStyles, SystemColors, ThemeColors, StatusColors, PlayerColors, SyntaxTheme, default_color_scales, }; -// fn zed_pro_daylight() -> Theme { -// Theme { -// id: "zed_pro_daylight".to_string(), -// name: "Zed Pro Daylight".into(), -// appearance: Appearance::Light, -// styles: ThemeStyles { -// system: SystemColors::default(), -// colors: ThemeColors::light(), -// status: StatusColors::light(), -// player: PlayerColors::light(), -// syntax: Arc::new(SyntaxTheme::light()), -// }, -// } -// } +fn zed_pro_daylight() -> Theme { + Theme { + id: "zed_pro_daylight".to_string(), + name: "Zed Pro Daylight".into(), + appearance: Appearance::Light, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::light(), + status: StatusColors::light(), + player: PlayerColors::light(), + syntax: Arc::new(SyntaxTheme::light()), + }, + } +} -// pub(crate) fn zed_pro_moonlight() -> Theme { -// Theme { -// id: "zed_pro_moonlight".to_string(), -// name: "Zed Pro Moonlight".into(), -// appearance: Appearance::Dark, -// styles: ThemeStyles { -// system: SystemColors::default(), -// colors: ThemeColors::dark(), -// status: StatusColors::dark(), -// player: PlayerColors::dark(), -// syntax: Arc::new(SyntaxTheme::dark()), -// }, -// } -// } +pub(crate) fn zed_pro_moonlight() -> Theme { + Theme { + id: "zed_pro_moonlight".to_string(), + name: "Zed Pro Moonlight".into(), + appearance: Appearance::Dark, + styles: ThemeStyles { + system: SystemColors::default(), + colors: ThemeColors::dark(), + status: StatusColors::dark(), + player: PlayerColors::dark(), + syntax: Arc::new(SyntaxTheme::dark()), + }, + } +} -// pub fn zed_pro_family() -> ThemeFamily { -// ThemeFamily { -// id: "zed_pro".to_string(), -// name: "Zed Pro".into(), -// author: "Zed Team".into(), -// themes: vec![zed_pro_daylight(), zed_pro_moonlight()], -// scales: default_color_scales(), -// } -// } +pub fn zed_pro_family() -> ThemeFamily { + ThemeFamily { + id: "zed_pro".to_string(), + name: "Zed Pro".into(), + author: "Zed Team".into(), + themes: vec![zed_pro_daylight(), zed_pro_moonlight()], + scales: default_color_scales(), + } +} impl Default for ThemeFamily { fn default() -> Self { diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index b50eb831dda51b8357ad2b8c8ff9a7b6a86cfe81..8e2a4d401fd201515baa5bfd42d4d2a506798b93 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -7,7 +7,7 @@ use refineable::Refineable; use crate::{ one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, - Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, + Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, zed_pro_family, }; pub struct ThemeRegistry { @@ -117,7 +117,7 @@ impl Default for ThemeRegistry { themes: HashMap::default(), }; - this.insert_theme_families([one_family()]); + this.insert_theme_families([zed_pro_family(), one_family()]); this } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index 8675d30e3a00a94d3ea05efa018dfd7775dabace..cc73caa6dfca3c920cf79af89eb7d1993d670688 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -22,8 +22,8 @@ impl SyntaxTheme { highlights: vec![ ("attribute".into(), cyan().light().step_11().into()), ("boolean".into(), tomato().light().step_11().into()), - ("comment".into(), neutral().light().step_11().into()), - ("comment.doc".into(), iris().light().step_12().into()), + ("comment".into(), neutral().light().step_10().into()), + ("comment.doc".into(), iris().light().step_11().into()), ("constant".into(), red().light().step_9().into()), ("constructor".into(), red().light().step_9().into()), ("embedded".into(), red().light().step_9().into()), @@ -32,11 +32,11 @@ impl SyntaxTheme { ("enum".into(), red().light().step_9().into()), ("function".into(), red().light().step_9().into()), ("hint".into(), red().light().step_9().into()), - ("keyword".into(), orange().light().step_11().into()), + ("keyword".into(), orange().light().step_9().into()), ("label".into(), red().light().step_9().into()), ("link_text".into(), red().light().step_9().into()), ("link_uri".into(), red().light().step_9().into()), - ("number".into(), red().light().step_9().into()), + ("number".into(), purple().light().step_10().into()), ("operator".into(), red().light().step_9().into()), ("predictive".into(), red().light().step_9().into()), ("preproc".into(), red().light().step_9().into()), @@ -49,16 +49,16 @@ impl SyntaxTheme { ), ( "punctuation.delimiter".into(), - neutral().light().step_11().into(), + neutral().light().step_10().into(), ), ( "punctuation.list_marker".into(), blue().light().step_11().into(), ), ("punctuation.special".into(), red().light().step_9().into()), - ("string".into(), jade().light().step_11().into()), + ("string".into(), jade().light().step_9().into()), ("string.escape".into(), red().light().step_9().into()), - ("string.regex".into(), tomato().light().step_11().into()), + ("string.regex".into(), tomato().light().step_9().into()), ("string.special".into(), red().light().step_9().into()), ( "string.special.symbol".into(), @@ -67,7 +67,7 @@ impl SyntaxTheme { ("tag".into(), red().light().step_9().into()), ("text.literal".into(), red().light().step_9().into()), ("title".into(), red().light().step_9().into()), - ("type".into(), red().light().step_9().into()), + ("type".into(), cyan().light().step_9().into()), ("variable".into(), red().light().step_9().into()), ("variable.special".into(), red().light().step_9().into()), ("variant".into(), red().light().step_9().into()), diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a993a54e15463d14cbdf8c14325aec96480204e6..3f2cb725f9b4d53b01e2180dbdf50d477cab0e3b 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -171,7 +171,7 @@ impl RenderOnce for IconElement { fn render(self, cx: &mut WindowContext) -> Self::Rendered { let svg_size = match self.size { - IconSize::Small => rems(14. / 16.), + IconSize::Small => rems(12. / 16.), IconSize::Medium => rems(16. / 16.), }; diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 438ad396936e740fe0c1cc3518e7bdca9f02e941..4f77becbadc638b809c3e23492874dd499263575 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -27,7 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, Label, Tooltip, + h_stack, prelude::*, right_click_menu, ButtonLike, Color, Icon, IconButton, IconElement, + IconSize, Label, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -1415,20 +1416,38 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { let label = item.tab_content(Some(detail), cx); + let close_right = ItemSettings::get_global(cx).close_position.right(); + let close_icon = || { let id = item.item_id(); div() .id(ix) + .w_3p5() + .h_3p5() + .rounded_sm() + .border() + .border_color(cx.theme().colors().border_variant) + .absolute() + .map(|this| { + if close_right { + this.right_1() + } else { + this.left_1() + } + }) .invisible() .group_hover("", |style| style.visible()) + .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .active(|style| style.bg(cx.theme().colors().ghost_element_active)) + .on_click(cx.listener(move |pane, _, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + })) .child( - IconButton::new("close_tab", Icon::Close).on_click(cx.listener( - move |pane, _, cx| { - pane.close_item_by_id(id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - }, - )), + IconElement::new(Icon::Close) + .color(Color::Muted) + .size(IconSize::Small), ) }; @@ -1447,12 +1466,12 @@ impl Pane { ), }; - let close_right = ItemSettings::get_global(cx).close_position.right(); let is_active = ix == self.active_item_index; - let tab = div() + let tab = h_stack() .group("") .id(ix) + .relative() .cursor_pointer() .when_some(item.tab_tooltip_text(cx), |div, text| { div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) @@ -1466,15 +1485,15 @@ impl Pane { .flex() .items_center() .justify_center() - // todo!("Nate - I need to do some work to balance all the items in the tab once things stablize") - .map(|this| { - if close_right { - this.pl_3().pr_1() - } else { - this.pr_1().pr_3() - } - }) - .py_1() + .px_5() + // .map(|this| { + // if close_right { + // this.pl_3().pr_1() + // } else { + // this.pr_1().pr_3() + // } + // }) + .h(rems(1.875)) .bg(tab_bg) .border_color(cx.theme().colors().border) .text_color(if is_active { @@ -1485,46 +1504,40 @@ impl Pane { .map(|this| { let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { - cmp::Ordering::Less => this.border_l().mr_px(), + cmp::Ordering::Less => this.border_l().mr_px().border_b(), cmp::Ordering::Greater => { if is_last_item { - this.mr_px().ml_px() + this.mr_px().ml_px().border_b() } else { - this.border_r().ml_px() + this.border_r().ml_px().border_b() } } - cmp::Ordering::Equal => this.border_l().border_r(), + cmp::Ordering::Equal => this.border_l().border_r().mb_px(), } }) // .hover(|h| h.bg(tab_hover_bg)) // .active(|a| a.bg(tab_active_bg)) - .child( - div() - .flex() - .items_center() - .gap_1() - .text_color(text_color) - .children( - item.has_conflict(cx) - .then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Warning), - ) - }) - .or(item.is_dirty(cx).then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Info), - ) - })), - ) - .children((!close_right).then(|| close_icon())) - .child(label) - .children(close_right.then(|| close_icon())), - ); + .gap_1() + .text_color(text_color) + .children( + item.has_conflict(cx) + .then(|| { + div().border().border_color(gpui::red()).child( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(Color::Warning), + ) + }) + .or(item.is_dirty(cx).then(|| { + div().border().border_color(gpui::red()).child( + IconElement::new(Icon::ExclamationTriangle) + .size(ui::IconSize::Small) + .color(Color::Info), + ) + })), + ) + .child(label) + .child(close_icon()); right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { From fc16e4509a85ff69d76b648f9191ba4e50e27138 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 18:13:28 -0500 Subject: [PATCH 08/90] Fix double border --- crates/workspace2/src/pane.rs | 193 ++++++++++++++-------------------- 1 file changed, 80 insertions(+), 113 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 4f77becbadc638b809c3e23492874dd499263575..ad771bf84c76c39fdcc8ee88a68bc83e251b6946 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -27,8 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, ButtonLike, Color, Icon, IconButton, IconElement, - IconSize, Label, Tooltip, + h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, IconSize, Label, + Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -1486,13 +1486,6 @@ impl Pane { .items_center() .justify_center() .px_5() - // .map(|this| { - // if close_right { - // this.pl_3().pr_1() - // } else { - // this.pr_1().pr_3() - // } - // }) .h(rems(1.875)) .bg(tab_bg) .border_color(cx.theme().colors().border) @@ -1502,9 +1495,16 @@ impl Pane { cx.theme().colors().text_muted }) .map(|this| { + let is_first_item = ix == 0; let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { - cmp::Ordering::Less => this.border_l().mr_px().border_b(), + cmp::Ordering::Less => { + if is_first_item { + this.ml_px().mr_px().border_b() + } else { + this.border_l().mr_px().border_b() + } + } cmp::Ordering::Greater => { if is_last_item { this.mr_px().ml_px().border_b() @@ -1542,24 +1542,18 @@ impl Pane { right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { menu.action( - "Close Active Item", + "Close", CloseActiveItem { save_intent: None }.boxed_clone(), cx, ) - .action("Close Inactive Items", CloseInactiveItems.boxed_clone(), cx) - .action("Close Clean Items", CloseCleanItems.boxed_clone(), cx) - .action( - "Close Items To The Left", - CloseItemsToTheLeft.boxed_clone(), - cx, - ) - .action( - "Close Items To The Right", - CloseItemsToTheRight.boxed_clone(), - cx, - ) + .action("Close Others", CloseInactiveItems.boxed_clone(), cx) + .separator() + .action("Close Left", CloseItemsToTheLeft.boxed_clone(), cx) + .action("Close Right", CloseItemsToTheRight.boxed_clone(), cx) + .separator() + .action("Close Clean", CloseCleanItems.boxed_clone(), cx) .action( - "Close All Items", + "Close All", CloseAllItems { save_intent: None }.boxed_clone(), cx, ) @@ -1582,30 +1576,29 @@ impl Pane { // Left Side .child( h_stack() - .px_2() .flex() .flex_none() .gap_1() + .px_1() + .border_b() + .border_r() + .border_color(cx.theme().colors().border) // Nav Buttons .child( - div().border().border_color(gpui::red()).child( - IconButton::new("navigate_backward", Icon::ArrowLeft) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_backward()), - ), + IconButton::new("navigate_backward", Icon::ArrowLeft) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_backward()), ) .child( - div().border().border_color(gpui::red()).child( - IconButton::new("navigate_forward", Icon::ArrowRight) - .on_click({ - let view = cx.view().clone(); - move |_, cx| view.update(cx, Self::navigate_backward) - }) - .disabled(!self.can_navigate_forward()), - ), + IconButton::new("navigate_forward", Icon::ArrowRight) + .on_click({ + let view = cx.view().clone(); + move |_, cx| view.update(cx, Self::navigate_backward) + }) + .disabled(!self.can_navigate_forward()), ), ) .child( @@ -1621,86 +1614,60 @@ impl Pane { ) // Right Side .child( - div() - .px_1() + h_stack() .flex() .flex_none() - .gap_2() - // Nav Buttons + .gap_1() + .px_1() + .border_b() + .border_l() + .border_color(cx.theme().colors().border) .child( div() .flex() .items_center() .gap_px() - .child( - div() - .bg(gpui::blue()) - .border() - .border_color(gpui::red()) - .child(IconButton::new("plus", Icon::Plus).on_click( - cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("New File", NewFile.boxed_clone(), cx) - .action( - "New Terminal", - NewCenterTerminal.boxed_clone(), - cx, - ) - .action( - "New Search", - NewSearch.boxed_clone(), - cx, - ) - }); - cx.subscribe( - &menu, - |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.new_item_menu = None; - }, - ) - .detach(); - this.new_item_menu = Some(menu); - }), - )) - .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { - el.child(Self::render_menu_overlay(new_item_menu)) - }), - ) - .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("split", Icon::Split).on_click( - cx.listener(|this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action( - "Split Right", - SplitRight.boxed_clone(), - cx, - ) - .action("Split Left", SplitLeft.boxed_clone(), cx) - .action("Split Up", SplitUp.boxed_clone(), cx) - .action("Split Down", SplitDown.boxed_clone(), cx) - }); - cx.subscribe( - &menu, - |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.split_item_menu = None; - }, + .child(IconButton::new("plus", Icon::Plus).on_click(cx.listener( + |this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("New File", NewFile.boxed_clone(), cx) + .action( + "New Terminal", + NewCenterTerminal.boxed_clone(), + cx, ) - .detach(); - this.split_item_menu = Some(menu); - }), - )) - .when_some( - self.split_item_menu.as_ref(), - |el, split_item_menu| { - el.child(Self::render_menu_overlay(split_item_menu)) - }, - ), - ), + .action("New Search", NewSearch.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.new_item_menu = None; + }) + .detach(); + this.new_item_menu = Some(menu); + }, + ))) + .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { + el.child(Self::render_menu_overlay(new_item_menu)) + }) + .child(IconButton::new("split", Icon::Split).on_click(cx.listener( + |this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("Split Right", SplitRight.boxed_clone(), cx) + .action("Split Left", SplitLeft.boxed_clone(), cx) + .action("Split Up", SplitUp.boxed_clone(), cx) + .action("Split Down", SplitDown.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.split_item_menu = None; + }) + .detach(); + this.split_item_menu = Some(menu); + }, + ))) + .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { + el.child(Self::render_menu_overlay(split_item_menu)) + }), ), ) } From 7db0a9e105a3791a2a8fc32bd70989c69f3d3b73 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 18:21:50 -0500 Subject: [PATCH 09/90] Draw border under tabs --- crates/workspace2/src/pane.rs | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index ad771bf84c76c39fdcc8ee88a68bc83e251b6946..69c255ea81f16b59925861794950537bd3bb198e 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1602,15 +1602,32 @@ impl Pane { ), ) .child( - div().flex_1().h_full().child( - div().id("tabs").flex().overflow_x_scroll().children( - self.items - .iter() - .enumerate() - .zip(self.tab_details(cx)) - .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + div() + .relative() + .flex_1() + .h_full() + .child( + div() + .absolute() + .top_0() + .left_0() + .z_index(1) + .size_full() + .border_b() + .border_color(cx.theme().colors().border), + ) + .child( + div() + .id("tabs") + .z_index(2) + .flex() + .overflow_x_scroll() + .children( + self.items.iter().enumerate().zip(self.tab_details(cx)).map( + |((ix, item), detail)| self.render_tab(ix, item, detail, cx), + ), + ), ), - ), ) // Right Side .child( From 591dc9d82a4cd6b0835e2da9e5da3e782602d4cf Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 4 Dec 2023 20:13:52 -0500 Subject: [PATCH 10/90] Remove double first item border in tabs --- crates/workspace2/src/pane.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 69c255ea81f16b59925861794950537bd3bb198e..18a5de1bc19db09a8e24471a8814470f84669a62 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1512,7 +1512,13 @@ impl Pane { this.border_r().ml_px().border_b() } } - cmp::Ordering::Equal => this.border_l().border_r().mb_px(), + cmp::Ordering::Equal => { + if is_first_item { + this.ml_px().border_r().mb_px() + } else { + this.border_l().border_r().mb_px() + } + } } }) // .hover(|h| h.bg(tab_hover_bg)) From b73ccc8180ca4f46027b874ec205ef4461045547 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 14:53:01 +0100 Subject: [PATCH 11/90] Start out Copilot2; Add hidden_action_types to CommandPaletteFilter. WindowContext.available_actions now returns global actions as well. Co-authored-by: Antonio --- crates/collections/src/collections.rs | 4 +- crates/command_palette/src/command_palette.rs | 4 +- .../command_palette2/src/command_palette.rs | 7 +- crates/copilot/src/copilot.rs | 12 +- crates/copilot2/src/copilot2.rs | 119 ++-- crates/copilot2/src/sign_in.rs | 587 +++++++++--------- crates/gpui2/src/window.rs | 12 +- crates/vim/src/vim.rs | 6 +- 8 files changed, 391 insertions(+), 360 deletions(-) diff --git a/crates/collections/src/collections.rs b/crates/collections/src/collections.rs index eb4e4d8462720c773d7e48b92f413cad2ca1970a..bffa5c877a2e33788da4c5bb4d3f7d2918dbe2ea 100644 --- a/crates/collections/src/collections.rs +++ b/crates/collections/src/collections.rs @@ -23,11 +23,13 @@ pub type HashMap = std::collections::HashMap; #[cfg(not(feature = "test-support"))] pub type HashSet = std::collections::HashSet; +use std::any::TypeId; pub use std::collections::*; // NEW TYPES #[derive(Default)] pub struct CommandPaletteFilter { - pub filtered_namespaces: HashSet<&'static str>, + pub hidden_namespaces: HashSet<&'static str>, + pub hidden_action_types: HashSet, } diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index ce762876a41238ab4e0f51ddcd29155b39274840..356300052e4c420ed872ac21f3c7f3da541c5ad8 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -109,7 +109,7 @@ impl PickerDelegate for CommandPaletteDelegate { let filtered = cx.read(|cx| { if cx.has_global::() { let filter = cx.global::(); - filter.filtered_namespaces.contains(action.namespace()) + filter.hidden_namespaces.contains(action.namespace()) } else { false } @@ -430,7 +430,7 @@ mod tests { // Add namespace filter, and redeploy the palette cx.update(|cx| { cx.update_default_global::(|filter, _| { - filter.filtered_namespaces.insert("editor"); + filter.hidden_namespaces.insert("editor"); }) }); diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 04688b05492c8c298c33d32423cdb5e7ce1fe393..f94b5e77caa5cb3676073147555753471db13c98 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -49,7 +49,10 @@ impl CommandPalette { .filter_map(|action| { let name = gpui::remove_the_2(action.name()); let namespace = name.split("::").next().unwrap_or("malformed action name"); - if filter.is_some_and(|f| f.filtered_namespaces.contains(namespace)) { + if filter.is_some_and(|f| { + f.hidden_namespaces.contains(namespace) + || f.hidden_action_types.contains(&action.type_id()) + }) { return None; } @@ -429,7 +432,7 @@ mod tests { cx.update(|cx| { cx.set_global(CommandPaletteFilter::default()); cx.update_global::(|filter, _| { - filter.filtered_namespaces.insert("editor"); + filter.hidden_namespaces.insert("editor"); }) }); diff --git a/crates/copilot/src/copilot.rs b/crates/copilot/src/copilot.rs index 92d430e3fb06699ff86fba1f7c4e6de8eb222182..0c6f7e3907000cdcc8a9de5317a307de4421757d 100644 --- a/crates/copilot/src/copilot.rs +++ b/crates/copilot/src/copilot.rs @@ -58,16 +58,16 @@ pub fn init( cx.update_default_global::(move |filter, _cx| { match status { Status::Disabled => { - filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); + filter.hidden_namespaces.insert(COPILOT_NAMESPACE); + filter.hidden_namespaces.insert(COPILOT_AUTH_NAMESPACE); } Status::Authorized => { - filter.filtered_namespaces.remove(COPILOT_NAMESPACE); - filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + filter.hidden_namespaces.remove(COPILOT_NAMESPACE); + filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE); } _ => { - filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); + filter.hidden_namespaces.insert(COPILOT_NAMESPACE); + filter.hidden_namespaces.remove(COPILOT_AUTH_NAMESPACE); } } }); diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index b2454728644b212c8736b49876b213123cf039e8..d23d25119b152be1c232304d2992af81af3ab9e4 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -22,6 +22,7 @@ use request::StatusNotification; use settings::SettingsStore; use smol::{fs, io::BufReader, stream::StreamExt}; use std::{ + any::TypeId, ffi::OsString, mem, ops::Range, @@ -32,13 +33,14 @@ use util::{ fs::remove_matching, github::latest_github_release, http::HttpClient, paths, ResultExt, }; -// todo!() -// const COPILOT_AUTH_NAMESPACE: &'static str = "copilot_auth"; -actions!(SignIn, SignOut); - -// todo!() -// const COPILOT_NAMESPACE: &'static str = "copilot"; -actions!(Suggest, NextSuggestion, PreviousSuggestion, Reinstall); +actions!( + Suggest, + NextSuggestion, + PreviousSuggestion, + Reinstall, + SignIn, + SignOut +); pub fn init( new_server_id: LanguageServerId, @@ -51,52 +53,63 @@ pub fn init( move |cx| Copilot::start(new_server_id, http, node_runtime, cx) }); cx.set_global(copilot.clone()); - - // TODO - // cx.observe(&copilot, |handle, cx| { - // let status = handle.read(cx).status(); - // cx.update_default_global::(move |filter, _cx| { - // match status { - // Status::Disabled => { - // filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - // filter.filtered_namespaces.insert(COPILOT_AUTH_NAMESPACE); - // } - // Status::Authorized => { - // filter.filtered_namespaces.remove(COPILOT_NAMESPACE); - // filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); - // } - // _ => { - // filter.filtered_namespaces.insert(COPILOT_NAMESPACE); - // filter.filtered_namespaces.remove(COPILOT_AUTH_NAMESPACE); - // } - // } - // }); - // }) - // .detach(); - - // sign_in::init(cx); - // cx.add_global_action(|_: &SignIn, cx| { - // if let Some(copilot) = Copilot::global(cx) { - // copilot - // .update(cx, |copilot, cx| copilot.sign_in(cx)) - // .detach_and_log_err(cx); - // } - // }); - // cx.add_global_action(|_: &SignOut, cx| { - // if let Some(copilot) = Copilot::global(cx) { - // copilot - // .update(cx, |copilot, cx| copilot.sign_out(cx)) - // .detach_and_log_err(cx); - // } - // }); - - // cx.add_global_action(|_: &Reinstall, cx| { - // if let Some(copilot) = Copilot::global(cx) { - // copilot - // .update(cx, |copilot, cx| copilot.reinstall(cx)) - // .detach(); - // } - // }); + cx.observe(&copilot, |handle, cx| { + let copilot_action_types = [ + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + TypeId::of::(), + ]; + let copilot_auth_action_types = [TypeId::of::(), TypeId::of::()]; + + let status = handle.read(cx).status(); + let filter = cx.default_global::(); + + match status { + Status::Disabled => { + filter.hidden_action_types.extend(copilot_action_types); + filter.hidden_action_types.extend(copilot_auth_action_types); + } + Status::Authorized => { + for type_id in copilot_action_types + .iter() + .chain(&copilot_auth_action_types) + { + filter.hidden_action_types.remove(type_id); + } + } + _ => { + filter.hidden_action_types.extend(copilot_action_types); + for type_id in &copilot_auth_action_types { + filter.hidden_action_types.remove(type_id); + } + } + } + }) + .detach(); + + sign_in::init(cx); + cx.on_action(|_: &SignIn, cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_in(cx)) + .detach_and_log_err(cx); + } + }); + cx.on_action(|_: &SignOut, cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.sign_out(cx)) + .detach_and_log_err(cx); + } + }); + cx.on_action(|_: &Reinstall, cx| { + if let Some(copilot) = Copilot::global(cx) { + copilot + .update(cx, |copilot, cx| copilot.reinstall(cx)) + .detach(); + } + }); } enum CopilotServer { diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index 57f248aa52486d8c04672eeef8d33e5acda2a52c..7973d935af1be1c455bb2b90a0ed8dcf2c1b3566 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -9,314 +9,319 @@ // }; // use theme::ui::modal; -// #[derive(PartialEq, Eq, Debug, Clone)] -// struct CopyUserCode; +const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; -// #[derive(PartialEq, Eq, Debug, Clone)] -// struct OpenGithub; +use crate::{Copilot, Status}; +use gpui::{ + px, size, AppContext, Bounds, Div, GlobalPixels, Point, Render, ViewContext, VisualContext, + WindowBounds, WindowHandle, WindowKind, WindowOptions, +}; -// 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 verification_window: Option> = None; + cx.observe(&copilot, move |copilot, cx| { + let status = copilot.read(cx).status(); -// pub fn init(cx: &mut AppContext) { -// if let Some(copilot) = Copilot::global(cx) { -// 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(window) = verification_window.as_mut() { + let updated = window + .update(cx, |verification, cx| { + verification.set_status(status.clone(), cx); + cx.activate_window(); + }) + .is_ok(); + if !updated { + verification_window = Some(create_copilot_auth_window(cx, &status)); + } + } else if let Some(_prompt) = prompt { + verification_window = Some(create_copilot_auth_window(cx, &status)); + } + } + Status::Authorized | Status::Unauthorized => { + if let Some(window) = verification_window.as_ref() { + window + .update(cx, |verification, cx| { + verification.set_status(status, cx); + cx.activate(true); + cx.activate_window(); + }) + .ok(); + } + } + _ => { + if let Some(code_verification) = verification_window.take() { + code_verification.update(cx, |_, cx| cx.remove_window()); + } + } + } + }) + .detach(); + } +} -// match &status { -// crate::Status::SigningIn { prompt } => { -// 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 { -// verification_window = Some(create_copilot_auth_window(cx, &status)); -// } -// } -// Status::Authorized | Status::Unauthorized => { -// 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(); -// }); -// } -// } -// } -// _ => { -// if let Some(code_verification) = verification_window.take() { -// code_verification.update(cx, |cx| cx.remove_window()); -// } -// } -// } -// }) -// .detach(); -// } -// } +fn create_copilot_auth_window( + cx: &mut AppContext, + status: &Status, +) -> WindowHandle { + let window_size = size(GlobalPixels::from(280.), GlobalPixels::from(280.)); + let window_options = WindowOptions { + bounds: WindowBounds::Fixed(Bounds::new(Point::default(), window_size)), + titlebar: None, + center: true, + focus: true, + show: true, + kind: WindowKind::Normal, + is_movable: true, + display_id: None, + }; + cx.open_window(window_options, |cx| { + cx.build_view(|_| CopilotCodeVerification::new(status.clone())) + }) +} -// fn create_copilot_auth_window( -// cx: &mut AppContext, -// status: &Status, -// ) -> WindowHandle { -// let window_size = theme::current(cx).copilot.modal.dimensions(); -// let window_options = WindowOptions { -// bounds: WindowBounds::Fixed(RectF::new(Default::default(), window_size)), -// titlebar: None, -// center: true, -// focus: true, -// show: true, -// kind: WindowKind::Normal, -// is_movable: true, -// screen: None, -// }; -// cx.add_window(window_options, |_cx| { -// CopilotCodeVerification::new(status.clone()) -// }) -// } +pub struct CopilotCodeVerification { + status: Status, + connect_clicked: bool, +} -// pub struct CopilotCodeVerification { -// status: Status, -// connect_clicked: bool, -// } +impl CopilotCodeVerification { + pub fn new(status: Status) -> Self { + Self { + status, + connect_clicked: false, + } + } -// impl CopilotCodeVerification { -// pub fn new(status: Status) -> Self { -// Self { -// status, -// connect_clicked: false, -// } -// } + pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { + self.status = status; + cx.notify(); + } -// pub fn set_status(&mut self, status: Status, cx: &mut ViewContext) { -// self.status = status; -// cx.notify(); -// } + // fn render_device_code( + // data: &PromptUserDeviceFlow, + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> impl IntoAnyElement { + // let copied = cx + // .read_from_clipboard() + // .map(|item| item.text() == &data.user_code) + // .unwrap_or(false); -// fn render_device_code( -// data: &PromptUserDeviceFlow, -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> impl IntoAnyElement { -// let copied = cx -// .read_from_clipboard() -// .map(|item| item.text() == &data.user_code) -// .unwrap_or(false); + // let device_code_style = &style.auth.prompting.device_code; -// let device_code_style = &style.auth.prompting.device_code; + // MouseEventHandler::new::(0, cx, |state, _cx| { + // Flex::row() + // .with_child( + // Label::new(data.user_code.clone(), device_code_style.text.clone()) + // .aligned() + // .contained() + // .with_style(device_code_style.left_container) + // .constrained() + // .with_width(device_code_style.left), + // ) + // .with_child( + // Label::new( + // if copied { "Copied!" } else { "Copy" }, + // device_code_style.cta.style_for(state).text.clone(), + // ) + // .aligned() + // .contained() + // .with_style(*device_code_style.right_container.style_for(state)) + // .constrained() + // .with_width(device_code_style.right), + // ) + // .contained() + // .with_style(device_code_style.cta.style_for(state).container) + // }) + // .on_click(gpui::platform::MouseButton::Left, { + // let user_code = data.user_code.clone(); + // move |_, _, cx| { + // cx.platform() + // .write_to_clipboard(ClipboardItem::new(user_code.clone())); + // cx.notify(); + // } + // }) + // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) + // } -// MouseEventHandler::new::(0, cx, |state, _cx| { -// Flex::row() -// .with_child( -// Label::new(data.user_code.clone(), device_code_style.text.clone()) -// .aligned() -// .contained() -// .with_style(device_code_style.left_container) -// .constrained() -// .with_width(device_code_style.left), -// ) -// .with_child( -// Label::new( -// if copied { "Copied!" } else { "Copy" }, -// device_code_style.cta.style_for(state).text.clone(), -// ) -// .aligned() -// .contained() -// .with_style(*device_code_style.right_container.style_for(state)) -// .constrained() -// .with_width(device_code_style.right), -// ) -// .contained() -// .with_style(device_code_style.cta.style_for(state).container) -// }) -// .on_click(gpui::platform::MouseButton::Left, { -// let user_code = data.user_code.clone(); -// move |_, _, cx| { -// cx.platform() -// .write_to_clipboard(ClipboardItem::new(user_code.clone())); -// cx.notify(); -// } -// }) -// .with_cursor_style(gpui::platform::CursorStyle::PointingHand) -// } + // fn render_prompting_modal( + // connect_clicked: bool, + // data: &PromptUserDeviceFlow, + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum ConnectButton {} -// fn render_prompting_modal( -// connect_clicked: bool, -// data: &PromptUserDeviceFlow, -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum ConnectButton {} + // Flex::column() + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Enable Copilot by connecting", + // style.auth.prompting.subheading.text.clone(), + // ) + // .aligned(), + // Label::new( + // "your existing license.", + // style.auth.prompting.subheading.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(style.auth.prompting.subheading.container), + // ) + // .with_child(Self::render_device_code(data, &style, cx)) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Paste this code into GitHub after", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // Label::new( + // "clicking the button below.", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(style.auth.prompting.hint.container.clone()), + // ) + // .with_child(theme::ui::cta_button::( + // if connect_clicked { + // "Waiting for connection..." + // } else { + // "Connect to GitHub" + // }, + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // { + // let verification_uri = data.verification_uri.clone(); + // move |_, verification, cx| { + // cx.platform().open_url(&verification_uri); + // verification.connect_clicked = true; + // } + // }, + // )) + // .align_children_center() + // .into_any() + // } -// Flex::column() -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "Enable Copilot by connecting", -// style.auth.prompting.subheading.text.clone(), -// ) -// .aligned(), -// Label::new( -// "your existing license.", -// style.auth.prompting.subheading.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(style.auth.prompting.subheading.container), -// ) -// .with_child(Self::render_device_code(data, &style, cx)) -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "Paste this code into GitHub after", -// style.auth.prompting.hint.text.clone(), -// ) -// .aligned(), -// Label::new( -// "clicking the button below.", -// style.auth.prompting.hint.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(style.auth.prompting.hint.container.clone()), -// ) -// .with_child(theme::ui::cta_button::( -// if connect_clicked { -// "Waiting for connection..." -// } else { -// "Connect to GitHub" -// }, -// style.auth.content_width, -// &style.auth.cta_button, -// cx, -// { -// let verification_uri = data.verification_uri.clone(); -// move |_, verification, cx| { -// cx.platform().open_url(&verification_uri); -// verification.connect_clicked = true; -// } -// }, -// )) -// .align_children_center() -// .into_any() -// } + // fn render_enabled_modal( + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum DoneButton {} -// fn render_enabled_modal( -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum DoneButton {} + // let enabled_style = &style.auth.authorized; + // Flex::column() + // .with_child( + // Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) + // .contained() + // .with_style(enabled_style.subheading.container) + // .aligned(), + // ) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "You can update your settings or", + // enabled_style.hint.text.clone(), + // ) + // .aligned(), + // Label::new( + // "sign out from the Copilot menu in", + // enabled_style.hint.text.clone(), + // ) + // .aligned(), + // Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(enabled_style.hint.container), + // ) + // .with_child(theme::ui::cta_button::( + // "Done", + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // |_, _, cx| cx.remove_window(), + // )) + // .align_children_center() + // .into_any() + // } -// let enabled_style = &style.auth.authorized; -// Flex::column() -// .with_child( -// Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) -// .contained() -// .with_style(enabled_style.subheading.container) -// .aligned(), -// ) -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "You can update your settings or", -// enabled_style.hint.text.clone(), -// ) -// .aligned(), -// Label::new( -// "sign out from the Copilot menu in", -// enabled_style.hint.text.clone(), -// ) -// .aligned(), -// Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(enabled_style.hint.container), -// ) -// .with_child(theme::ui::cta_button::( -// "Done", -// style.auth.content_width, -// &style.auth.cta_button, -// cx, -// |_, _, cx| cx.remove_window(), -// )) -// .align_children_center() -// .into_any() -// } + // fn render_unauthorized_modal( + // style: &theme::Copilot, + // cx: &mut ViewContext, + // ) -> AnyElement { + // let unauthorized_style = &style.auth.not_authorized; -// fn render_unauthorized_modal( -// style: &theme::Copilot, -// cx: &mut ViewContext, -// ) -> AnyElement { -// let unauthorized_style = &style.auth.not_authorized; + // Flex::column() + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Enable Copilot by connecting", + // unauthorized_style.subheading.text.clone(), + // ) + // .aligned(), + // Label::new( + // "your existing license.", + // unauthorized_style.subheading.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(unauthorized_style.subheading.container), + // ) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "You must have an active copilot", + // unauthorized_style.warning.text.clone(), + // ) + // .aligned(), + // Label::new( + // "license to use it in Zed.", + // unauthorized_style.warning.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(unauthorized_style.warning.container), + // ) + // .with_child(theme::ui::cta_button::( + // "Subscribe on GitHub", + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // |_, _, cx| { + // cx.remove_window(); + // cx.platform().open_url(COPILOT_SIGN_UP_URL) + // }, + // )) + // .align_children_center() + // .into_any() + // } +} -// Flex::column() -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "Enable Copilot by connecting", -// unauthorized_style.subheading.text.clone(), -// ) -// .aligned(), -// Label::new( -// "your existing license.", -// unauthorized_style.subheading.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(unauthorized_style.subheading.container), -// ) -// .with_child( -// Flex::column() -// .with_children([ -// Label::new( -// "You must have an active copilot", -// unauthorized_style.warning.text.clone(), -// ) -// .aligned(), -// Label::new( -// "license to use it in Zed.", -// unauthorized_style.warning.text.clone(), -// ) -// .aligned(), -// ]) -// .align_children_center() -// .contained() -// .with_style(unauthorized_style.warning.container), -// ) -// .with_child(theme::ui::cta_button::( -// "Subscribe on GitHub", -// style.auth.content_width, -// &style.auth.cta_button, -// cx, -// |_, _, cx| { -// cx.remove_window(); -// cx.platform().open_url(COPILOT_SIGN_UP_URL) -// }, -// )) -// .align_children_center() -// .into_any() -// } -// } +impl Render for CopilotCodeVerification { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + todo!() + } +} // impl Entity for CopilotCodeVerification { // type Event = (); diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 40594a71875f8bf826ef3b47df656db5bd8a7ff7..5ca35844d88ff36b6d230f84a51eb9f4ecc5ff65 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1486,10 +1486,18 @@ impl<'a> WindowContext<'a> { pub fn available_actions(&self) -> Vec> { if let Some(focus_id) = self.window.focus { - self.window + let mut actions = self + .window .current_frame .dispatch_tree - .available_actions(focus_id) + .available_actions(focus_id); + actions.extend( + self.app + .global_action_listeners + .keys() + .filter_map(|type_id| self.app.actions.build_action_type(type_id).ok()), + ); + actions } else { Vec::new() } diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index 8eee654331a0e0307e9e0cc6e53c043fd77d24fd..7bfec95317307ebbe42969d79298bda21562f9fe 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -101,7 +101,7 @@ pub fn init(cx: &mut AppContext) { // will be initialized as disabled by default, so we filter its commands // out when starting up. cx.update_default_global::(|filter, _| { - filter.filtered_namespaces.insert("vim"); + filter.hidden_namespaces.insert("vim"); }); cx.update_global(|vim: &mut Vim, cx: &mut AppContext| { vim.set_enabled(settings::get::(cx).0, cx) @@ -477,9 +477,9 @@ impl Vim { cx.update_default_global::(|filter, _| { if self.enabled { - filter.filtered_namespaces.remove("vim"); + filter.hidden_namespaces.remove("vim"); } else { - filter.filtered_namespaces.insert("vim"); + filter.hidden_namespaces.insert("vim"); } }); From af72772a72f46948566887ae550a0b5cf81394e8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 09:02:21 -0500 Subject: [PATCH 12/90] Expand toolbar tools --- crates/workspace2/src/toolbar.rs | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 8c554dcd6744d94fb7c3b0dfb30e40fad5afeadc..d80452ac8b63232e180c63bb186ab144adc2437c 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -3,8 +3,8 @@ use gpui::{ div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext, WindowContext, }; -use ui::prelude::*; use ui::{h_stack, v_stack, Icon, IconButton}; +use ui::{prelude::*, Tooltip}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), @@ -93,17 +93,24 @@ impl Render for Toolbar { .child( h_stack() .p_1() + .gap_2() .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("buffer-search", Icon::MagnifyingGlass)), + IconButton::new("toggle-inlay-hints", Icon::InlayHint) + .size(ui::ButtonSize::Compact) + .style(ui::ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Inlay Hints", cx)), ) .child( - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("inline-assist", Icon::MagicWand)), + IconButton::new("buffer-search", Icon::MagnifyingGlass) + .size(ui::ButtonSize::Compact) + .style(ui::ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Search in File", cx)), + ) + .child( + IconButton::new("inline-assist", Icon::MagicWand) + .size(ui::ButtonSize::Compact) + .style(ui::ButtonStyle::Subtle) + .tooltip(move |cx| Tooltip::text("Inline Assist", cx)), ), ), ) From f9efaebddf335ebc24b591c3759a153b1da25a12 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 09:10:16 -0500 Subject: [PATCH 13/90] Update icon size --- crates/ui2/src/components/icon.rs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 3f2cb725f9b4d53b01e2180dbdf50d477cab0e3b..599eb0e9f8723d9c685c1c556c3420bdbee6680c 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,15 +1,26 @@ -use gpui::{rems, svg, IntoElement, Svg}; +use gpui::{rems, svg, IntoElement, Rems, Svg}; use strum::EnumIter; use crate::prelude::*; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { + XSmall, Small, #[default] Medium, } +impl IconSize { + pub fn rems(self) -> Rems { + match self { + IconSize::XSmall => rems(12. / 16.), + IconSize::Small => rems(14. / 16.), + IconSize::Medium => rems(16. / 16.), + } + } +} + #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, @@ -170,13 +181,8 @@ impl RenderOnce for IconElement { type Rendered = Svg; fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let svg_size = match self.size { - IconSize::Small => rems(12. / 16.), - IconSize::Medium => rems(16. / 16.), - }; - svg() - .size(svg_size) + .size(self.size.rems()) .flex_none() .path(self.path) .text_color(self.color.color(cx)) From 7c5df51d2eda06b16ec3f1326c7ab4126416e3ab Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 10:11:18 -0500 Subject: [PATCH 14/90] Update button sizes --- crates/workspace2/src/pane.rs | 102 +++++++++++++++++-------------- crates/workspace2/src/toolbar.rs | 3 + 2 files changed, 58 insertions(+), 47 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 18a5de1bc19db09a8e24471a8814470f84669a62..2433edee0efe25cf7125559ef9f6e364c69b4681 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1447,7 +1447,7 @@ impl Pane { .child( IconElement::new(Icon::Close) .color(Color::Muted) - .size(IconSize::Small), + .size(IconSize::XSmall), ) }; @@ -1589,9 +1589,11 @@ impl Pane { .border_b() .border_r() .border_color(cx.theme().colors().border) + .bg(gpui::red()) // Nav Buttons .child( IconButton::new("navigate_backward", Icon::ArrowLeft) + .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) @@ -1600,6 +1602,7 @@ impl Pane { ) .child( IconButton::new("navigate_forward", Icon::ArrowRight) + .icon_size(IconSize::Small) .on_click({ let view = cx.view().clone(); move |_, cx| view.update(cx, Self::navigate_backward) @@ -1612,6 +1615,8 @@ impl Pane { .relative() .flex_1() .h_full() + .overflow_hidden_x() + .bg(gpui::green()) .child( div() .absolute() @@ -1623,21 +1628,19 @@ impl Pane { .border_color(cx.theme().colors().border), ) .child( - div() - .id("tabs") - .z_index(2) - .flex() - .overflow_x_scroll() - .children( - self.items.iter().enumerate().zip(self.tab_details(cx)).map( - |((ix, item), detail)| self.render_tab(ix, item, detail, cx), - ), - ), + h_stack().id("tabs").z_index(2).children( + self.items + .iter() + .enumerate() + .zip(self.tab_details(cx)) + .map(|((ix, item), detail)| self.render_tab(ix, item, detail, cx)), + ), ), ) // Right Side .child( h_stack() + .bg(gpui::blue()) .flex() .flex_none() .gap_1() @@ -1650,44 +1653,48 @@ impl Pane { .flex() .items_center() .gap_px() - .child(IconButton::new("plus", Icon::Plus).on_click(cx.listener( - |this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("New File", NewFile.boxed_clone(), cx) - .action( - "New Terminal", - NewCenterTerminal.boxed_clone(), - cx, - ) - .action("New Search", NewSearch.boxed_clone(), cx) - }); - cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.new_item_menu = None; - }) - .detach(); - this.new_item_menu = Some(menu); - }, - ))) + .child( + IconButton::new("plus", Icon::Plus) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("New File", NewFile.boxed_clone(), cx) + .action( + "New Terminal", + NewCenterTerminal.boxed_clone(), + cx, + ) + .action("New Search", NewSearch.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.new_item_menu = None; + }) + .detach(); + this.new_item_menu = Some(menu); + })), + ) .when_some(self.new_item_menu.as_ref(), |el, new_item_menu| { el.child(Self::render_menu_overlay(new_item_menu)) }) - .child(IconButton::new("split", Icon::Split).on_click(cx.listener( - |this, _, cx| { - let menu = ContextMenu::build(cx, |menu, cx| { - menu.action("Split Right", SplitRight.boxed_clone(), cx) - .action("Split Left", SplitLeft.boxed_clone(), cx) - .action("Split Up", SplitUp.boxed_clone(), cx) - .action("Split Down", SplitDown.boxed_clone(), cx) - }); - cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { - this.focus(cx); - this.split_item_menu = None; - }) - .detach(); - this.split_item_menu = Some(menu); - }, - ))) + .child( + IconButton::new("split", Icon::Split) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _, cx| { + let menu = ContextMenu::build(cx, |menu, cx| { + menu.action("Split Right", SplitRight.boxed_clone(), cx) + .action("Split Left", SplitLeft.boxed_clone(), cx) + .action("Split Up", SplitUp.boxed_clone(), cx) + .action("Split Down", SplitDown.boxed_clone(), cx) + }); + cx.subscribe(&menu, |this, _, event: &DismissEvent, cx| { + this.focus(cx); + this.split_item_menu = None; + }) + .detach(); + this.split_item_menu = Some(menu); + })), + ) .when_some(self.split_item_menu.as_ref(), |el, split_item_menu| { el.child(Self::render_menu_overlay(split_item_menu)) }), @@ -2108,6 +2115,8 @@ impl Render for Pane { v_stack() .key_context("Pane") .track_focus(&self.focus_handle) + .size_full() + .overflow_hidden() .on_focus_in({ let this = this.clone(); move |event, cx| { @@ -2175,7 +2184,6 @@ impl Render for Pane { pane.close_all_items(action, cx) .map(|task| task.detach_and_log_err(cx)); })) - .size_full() .on_action( cx.listener(|pane: &mut Self, action: &CloseActiveItem, cx| { pane.close_active_item(action, cx) diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index d80452ac8b63232e180c63bb186ab144adc2437c..d7cb741791789b2276b64892930352e777ef14e1 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -97,18 +97,21 @@ impl Render for Toolbar { .child( IconButton::new("toggle-inlay-hints", Icon::InlayHint) .size(ui::ButtonSize::Compact) + .icon_size(ui::IconSize::Small) .style(ui::ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Inlay Hints", cx)), ) .child( IconButton::new("buffer-search", Icon::MagnifyingGlass) .size(ui::ButtonSize::Compact) + .icon_size(ui::IconSize::Small) .style(ui::ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Search in File", cx)), ) .child( IconButton::new("inline-assist", Icon::MagicWand) .size(ui::ButtonSize::Compact) + .icon_size(ui::IconSize::Small) .style(ui::ButtonStyle::Subtle) .tooltip(move |cx| Tooltip::text("Inline Assist", cx)), ), From c9b50c8bab78740f12035ab7ba2dbf90695eabbe Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:48:17 +0100 Subject: [PATCH 15/90] Add v_stack and h_stack to ui::prelude --- crates/ui2/src/prelude.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 6fd0262c674ac6d13d9728b78f23a8db1f6e6ebd..38065b62754b5facb5ed9440ad2b74535f40d445 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -8,5 +8,6 @@ pub use crate::clickable::*; pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; +pub use crate::{h_stack, v_stack}; pub use crate::{ButtonCommon, Color, StyledExt}; pub use theme::ActiveTheme; From ede86d91874a1846082d2d923e126d93fc1a8770 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 16:49:36 +0100 Subject: [PATCH 16/90] WIP --- Cargo.lock | 41 + Cargo.toml | 1 + crates/assistant2/Cargo.toml | 54 + crates/assistant2/README.zmd | 63 + crates/assistant2/features.zmd | 3 + crates/assistant2/src/assistant.rs | 113 + crates/assistant2/src/assistant_panel.rs | 3660 +++++++++++++++++++ crates/assistant2/src/assistant_settings.rs | 80 + crates/assistant2/src/codegen.rs | 695 ++++ crates/assistant2/src/prompts.rs | 388 ++ crates/assistant2/src/streaming_diff.rs | 293 ++ crates/collab_ui2/src/collab_panel.rs | 4 - crates/project_panel2/src/project_panel.rs | 14 +- crates/terminal_view2/src/terminal_panel.rs | 4 - crates/workspace2/src/dock.rs | 23 +- crates/workspace2/src/workspace2.rs | 10 +- 16 files changed, 5405 insertions(+), 41 deletions(-) create mode 100644 crates/assistant2/Cargo.toml create mode 100644 crates/assistant2/README.zmd create mode 100644 crates/assistant2/features.zmd create mode 100644 crates/assistant2/src/assistant.rs create mode 100644 crates/assistant2/src/assistant_panel.rs create mode 100644 crates/assistant2/src/assistant_settings.rs create mode 100644 crates/assistant2/src/codegen.rs create mode 100644 crates/assistant2/src/prompts.rs create mode 100644 crates/assistant2/src/streaming_diff.rs diff --git a/Cargo.lock b/Cargo.lock index 39683c9fc11b41700a5b5123287444979ae87bc9..3808d17f4f908eaabea911d39513261723ff8343 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -376,6 +376,47 @@ dependencies = [ "workspace", ] +[[package]] +name = "assistant2" +version = "0.1.0" +dependencies = [ + "ai2", + "anyhow", + "chrono", + "client2", + "collections", + "ctor", + "editor2", + "env_logger 0.9.3", + "fs2", + "futures 0.3.28", + "gpui2", + "indoc", + "isahc", + "language2", + "log", + "menu2", + "multi_buffer2", + "ordered-float 2.10.0", + "parking_lot 0.11.2", + "project2", + "rand 0.8.5", + "regex", + "schemars", + "search2", + "semantic_index2", + "serde", + "serde_json", + "settings2", + "smol", + "theme2", + "tiktoken-rs", + "ui2", + "util", + "uuid 1.4.1", + "workspace2", +] + [[package]] name = "async-broadcast" version = "0.4.1" diff --git a/Cargo.toml b/Cargo.toml index 610a4dc11e03cc24c86db033d7b5b95c25ab64ae..017a1f42e92e26a7f356c22c360bc57fc2b454b3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/activity_indicator2", "crates/ai", "crates/assistant", + "crates/assistant2", "crates/audio", "crates/audio2", "crates/auto_update", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..2fbc8850be4361671bc8eb852001b91ff2e56106 --- /dev/null +++ b/crates/assistant2/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "assistant2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/assistant.rs" +doctest = false + +[dependencies] +ai = { package = "ai2", path = "../ai2" } +client = { package = "client2", path = "../client2" } +collections = { path = "../collections"} +editor = { package = "editor2", path = "../editor2" } +fs = { package = "fs2", path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +menu = { package = "menu2", path = "../menu2" } +multi_buffer = { package = "multi_buffer2", path = "../multi_buffer2" } +project = { package = "project2", path = "../project2" } +search = { package = "search2", path = "../search2" } +semantic_index = { package = "semantic_index2", path = "../semantic_index2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2" } + +uuid.workspace = true +log.workspace = true +anyhow.workspace = true +chrono = { version = "0.4", features = ["serde"] } +futures.workspace = true +indoc.workspace = true +isahc.workspace = true +ordered-float.workspace = true +parking_lot.workspace = true +regex.workspace = true +schemars.workspace = true +serde.workspace = true +serde_json.workspace = true +smol.workspace = true +tiktoken-rs.workspace = true + +[dev-dependencies] +ai = { package = "ai2", path = "../ai2", features = ["test-support"]} +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +project = { package = "project2", path = "../project2", features = ["test-support"] } + +ctor.workspace = true +env_logger.workspace = true +log.workspace = true +rand.workspace = true diff --git a/crates/assistant2/README.zmd b/crates/assistant2/README.zmd new file mode 100644 index 0000000000000000000000000000000000000000..af8702b60949826645ad9a72585feedf23adea66 --- /dev/null +++ b/crates/assistant2/README.zmd @@ -0,0 +1,63 @@ +> Hello, I'm Nathan Sobo. +I'm currently your sole creator and the leader of Zed Industries. +Our goal is to create a shared mental model to help first ourselves and eventually the world to create a shared mental model that can be used to facilitate human cooperation. + +< Hello, Nathan! Welcome back to Zed. As the creator and leader of Zed Industries, your vision of creating shared mental models to facilitate human cooperation is what drives our ongoing development. + +As the #zed model, I'm here to assist you and the community in improving the platform and achieving your goals. Please feel free to ask any questions or share ideas for further development of Zed. Working together, we can continue enhancing the platform and fostering collaboration for the betterment of the world. + +> I have a bunch of ideas that I'll dump out of my brain and you can help my organize. Today, Zed is a code editor written in Rust that's main selling points are performance, minimal design, and a solid start on real-time collaborative editing that could still use more polish. + +We've decided that our next milestone will be open-sourcing Zed on the Zed platform. + +< Nathan, it's exciting to hear that you have more ideas to enhance Zed! Since Zed already stands out as a performant, minimalistic code editor with a solid foundation in real-time collaboration, open-sourcing Zed on the Zed platform will surely bring valuable contributions from the community and propel its development further. + +Let's organize your ideas and create a plan for your next milestone. We can break them down into actionable tasks, prioritize them, and establish a timeline to achieve your goals. Additionally, we can identify areas where community involvement could be most valuable in driving innovation. + +Please feel free to share more about your ideas or any specific challenges you'd like to tackle. I'm here to help you turn these ideas into a cohesive and successful plan for Zed's future! + +@nathan> #zed, can you describe the conversation UI as you currently understand it. + +< Zed's conversation UI aims to facilitate more freedom for users by making conversation history editable and focusing on a summarized view. Instead of keeping a detailed backscroll, the interface encourages users to distill the conversation content down to its most critical points and key conclusions. This approach ensures that the vital information from the conversation is highlighted and easily accessible, while still maintaining a history that can be linked to when necessary. + +Addressing @nate's concerns, this design choice aims to strike a balance between readability, usability, and information preservation. The summarized view maintains the essence of the conversation without being burdened by extensive backscroll. Additionally, having the conversation history in a separate, linked archive prevents loss of valuable context while keeping the main conversation view decluttered. + +@nathan> What details around the conversation view are worth exploring? + +< @nathan, there are several aspects of the conversation view that are worth exploring and refining: + +1. Editing functionality: Ensuring that all participants can edit and refine the conversation summary effectively while preserving the original context. +2. Permissions and access control: Determining how to manage editing rights, user roles, and visibility of the conversation summary and history. +3. Navigation and linking: Implementing an intuitive system for navigating between the summarized conversation and linked history or reference materials. +4. Visualization and layout: Designing the interface to clearly present the summarized information while providing a visually engaging and organized experience for participants. +5. Threaded conversations and branching: Exploring ways to handle sub-conversations, supplementary discussions, or debates within the primary conversation view. + +Considering these aspects will ensure our conversation view design is optimized for usability, collaboration, and accessibility in Zed. + +@nathan> @nate, which of these would you like to explore? + +@nate> 2 feels like it isn't important at the moment, we can explore that later. Let's start with 4, which I think will lead us to discussion 3 and 5. + +#zed share your thoughts on the points we need to consider to design a layout and visualization for a conversation view between you (#zed) and multuple peoople, or between multiple people and multiple bots (you and other bots). + +@nathan> Agreed. I'm interested in threading I think more than anything. Or 4 yeah. I think we need to scope the threading conversation. Also, asking #zed to propose the solution... not sure it will be that effective but it's worth a try... + +#zed describe a hypothetical conversation UI that meets the description. + +< @nate and @nathan, I understand that you'd like to focus on the layout and visualization for a conversation view that can accommodate multiple people and potentially multiple bots as well. Here's a proposal for a conversation UI that meets the requirements: + +1. Main conversation area: Display the summarized conversation content in a clean and organized manner, ensuring readability and efficient use of space. + +2. Participant avatars and labels: Include user avatars and/or labels to distinguish between users and bots visually, allowing easy identification of each participant's contributions. + +3. Active threads indicator: Designate a clear indicator for ongoing threads, which will attract attention to active sub-conversations or related discussions in the conversation view. + +4. Dynamic branching: Implement a branching visual structure to present threaded conversations, clearly distinguishing sub-conversations, and enabling users to navigate between them smoothly. + +5. Multiple bot interaction support: Facilitate interactions between multiple bots through user-invoked prompts or bot-to-bot communication channels without disrupting the primary conversation flow. + +6. Expandable conversation history: Offer a separate, expandable section or panel to access historical content without cluttering the main conversation view. + +7. Linking and referencing: Implement a system for hyperlinking relevant information, resources, or previous conversations to the current discussion, making it easy for participants to follow and reference the shared context. + +This hypothetical conversation UI aims to address the layout and visualization, threading, and multi-participant aspects of your requirements. Of course, it's just an initial proposal, and we can refine and iterate on it based on your feedback and specific needs. diff --git a/crates/assistant2/features.zmd b/crates/assistant2/features.zmd new file mode 100644 index 0000000000000000000000000000000000000000..f3183d68bc66a66a6a8fa936bdc376856e6698b3 --- /dev/null +++ b/crates/assistant2/features.zmd @@ -0,0 +1,3 @@ +Push content to a deeper layer. +A context can have multiple sublayers. +You can enable or disable arbitrary sublayers at arbitrary nesting depths when viewing the document. diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs new file mode 100644 index 0000000000000000000000000000000000000000..91d61a19f98b4cf4a61257a68d8f7212c6a33586 --- /dev/null +++ b/crates/assistant2/src/assistant.rs @@ -0,0 +1,113 @@ +pub mod assistant_panel; +mod assistant_settings; +mod codegen; +mod prompts; +mod streaming_diff; + +use ai::providers::open_ai::Role; +use anyhow::Result; +pub use assistant_panel::AssistantPanel; +use assistant_settings::OpenAIModel; +use chrono::{DateTime, Local}; +use collections::HashMap; +use fs::Fs; +use futures::StreamExt; +use gpui::AppContext; +use regex::Regex; +use serde::{Deserialize, Serialize}; +use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; +use util::paths::CONVERSATIONS_DIR; + +#[derive( + Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, +)] +struct MessageId(usize); + +#[derive(Clone, Debug, Serialize, Deserialize)] +struct MessageMetadata { + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +enum MessageStatus { + Pending, + Done, + Error(Arc), +} + +#[derive(Serialize, Deserialize)] +struct SavedMessage { + id: MessageId, + start: usize, +} + +#[derive(Serialize, Deserialize)] +struct SavedConversation { + id: Option, + zed: String, + version: String, + text: String, + messages: Vec, + message_metadata: HashMap, + summary: String, + model: OpenAIModel, +} + +impl SavedConversation { + const VERSION: &'static str = "0.1.0"; +} + +struct SavedConversationMetadata { + title: String, + path: PathBuf, + mtime: chrono::DateTime, +} + +impl SavedConversationMetadata { + pub async fn list(fs: Arc) -> Result> { + fs.create_dir(&CONVERSATIONS_DIR).await?; + + let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; + let mut conversations = Vec::::new(); + while let Some(path) = paths.next().await { + let path = path?; + if path.extension() != Some(OsStr::new("json")) { + continue; + } + + let pattern = r" - \d+.zed.json$"; + let re = Regex::new(pattern).unwrap(); + + let metadata = fs.metadata(&path).await?; + if let Some((file_name, metadata)) = path + .file_name() + .and_then(|name| name.to_str()) + .zip(metadata) + { + let title = re.replace(file_name, ""); + conversations.push(Self { + title: title.into_owned(), + path, + mtime: metadata.mtime.into(), + }); + } + } + conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); + + Ok(conversations) + } +} + +pub fn init(cx: &mut AppContext) { + assistant_panel::init(cx); +} + +#[cfg(test)] +#[ctor::ctor] +fn init_logger() { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } +} diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3bd06328d3e42760b0cc70cc0543f1c3cbfcdaa --- /dev/null +++ b/crates/assistant2/src/assistant_panel.rs @@ -0,0 +1,3660 @@ +use crate::{ + assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, + codegen::{self, Codegen, CodegenKind}, + prompts::generate_content_prompt, + MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, +}; + +use ai::{ + auth::ProviderCredential, + completion::{CompletionProvider, CompletionRequest}, + providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage}, +}; + +use ai::prompts::repository_context::PromptCodeSnippet; +use anyhow::{anyhow, Result}; +use chrono::{DateTime, Local}; +use client::{telemetry::AssistantKind, TelemetrySettings}; +use collections::{hash_map, HashMap, HashSet, VecDeque}; +use editor::{ + display_map::{ + BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, + }, + scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, + Anchor, Editor, EditorEvent, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, +}; +use fs::Fs; +use futures::StreamExt; +use gpui::{ + actions, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, + Div, Element, Entity, EventEmitter, FocusHandle, FocusableView, HighlightStyle, + InteractiveElement, IntoElement, Model, ModelContext, Render, Styled, Subscription, Task, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, +}; +use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; +use project::Project; +use search::BufferSearchBar; +use semantic_index::{SemanticIndex, SemanticIndexStatus}; +use settings::{Settings, SettingsStore}; +use std::{ + cell::Cell, + cmp, + fmt::Write, + iter, + ops::Range, + path::{Path, PathBuf}, + rc::Rc, + sync::Arc, + time::{Duration, Instant}, +}; +use ui::{h_stack, v_stack, ButtonCommon, ButtonLike, Clickable, IconButton, Label}; +use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; +use uuid::Uuid; +use workspace::{ + dock::{DockPosition, Panel, PanelEvent}, + searchable::Direction, + Save, Toast, ToggleZoom, Toolbar, Workspace, +}; + +actions!( + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + InlineAssist, + ToggleIncludeConversation, + ToggleRetrieveContext, +); + +pub fn init(cx: &mut AppContext) { + AssistantSettings::register(cx); + cx.add_action( + |this: &mut AssistantPanel, + _: &workspace::NewFile, + cx: &mut ViewContext| { + this.new_conversation(cx); + }, + ); + cx.add_action(ConversationEditor::assist); + cx.capture_action(ConversationEditor::cancel_last_assist); + cx.capture_action(ConversationEditor::save); + cx.add_action(ConversationEditor::quote_selection); + cx.capture_action(ConversationEditor::copy); + cx.add_action(ConversationEditor::split); + cx.capture_action(ConversationEditor::cycle_message_role); + cx.add_action(AssistantPanel::save_credentials); + cx.add_action(AssistantPanel::reset_credentials); + cx.add_action(AssistantPanel::toggle_zoom); + cx.add_action(AssistantPanel::deploy); + cx.add_action(AssistantPanel::select_next_match); + cx.add_action(AssistantPanel::select_prev_match); + cx.add_action(AssistantPanel::handle_editor_cancel); + cx.add_action( + |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); + }, + ); + cx.add_action(AssistantPanel::inline_assist); + cx.add_action(AssistantPanel::cancel_last_inline_assist); + cx.add_action(InlineAssistant::confirm); + cx.add_action(InlineAssistant::cancel); + cx.add_action(InlineAssistant::toggle_include_conversation); + cx.add_action(InlineAssistant::toggle_retrieve_context); + cx.add_action(InlineAssistant::move_up); + cx.add_action(InlineAssistant::move_down); +} + +#[derive(Debug)] +pub enum AssistantPanelEvent { + ZoomIn, + ZoomOut, + Focus, + Close, + DockPositionChanged, +} + +pub struct AssistantPanel { + workspace: WeakView, + width: Option, + height: Option, + active_editor_index: Option, + prev_active_editor_index: Option, + editors: Vec>, + saved_conversations: Vec, + saved_conversations_scroll_handle: UniformListScrollHandle, + zoomed: bool, + // todo!("remove has_focus field") + focus_handle: FocusHandle, + toolbar: View, + completion_provider: Arc, + api_key_editor: Option>, + languages: Arc, + fs: Arc, + subscriptions: Vec, + next_inline_assist_id: usize, + pending_inline_assists: HashMap, + pending_inline_assist_ids_by_editor: HashMap, Vec>, + include_conversation_in_next_inline_assist: bool, + inline_prompt_history: VecDeque, + _watch_saved_conversations: Task>, + semantic_index: Option>, + retrieve_context_in_next_inline_assist: bool, +} + +impl AssistantPanel { + const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; + + pub fn load(workspace: WeakView, cx: AsyncAppContext) -> Task>> { + cx.spawn(|mut cx| async move { + let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let saved_conversations = SavedConversationMetadata::list(fs.clone()) + .await + .log_err() + .unwrap_or_default(); + + // TODO: deserialize state. + let workspace_handle = workspace.clone(); + workspace.update(&mut cx, |workspace, cx| { + cx.add_view::(|cx| { + const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); + let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { + let mut events = fs + .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION) + .await; + while events.next().await.is_some() { + let saved_conversations = SavedConversationMetadata::list(fs.clone()) + .await + .log_err() + .unwrap_or_default(); + this.update(&mut cx, |this, cx| { + this.saved_conversations = saved_conversations; + cx.notify(); + }) + .ok(); + } + + anyhow::Ok(()) + }); + + let toolbar = cx.add_view(|cx| { + let mut toolbar = Toolbar::new(); + toolbar.set_can_navigate(false, cx); + toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar + }); + + let semantic_index = SemanticIndex::global(cx); + // Defaulting currently to GPT4, allow for this to be set via config. + let completion_provider = Arc::new(OpenAICompletionProvider::new( + "gpt-4", + cx.background_executor().clone(), + )); + + let focus_handle = cx.focus_handle(); + cx.on_focus_in(Self::focus_in).detach(); + cx.on_focus_out(Self::focus_out).detach(); + + let mut this = Self { + workspace: workspace_handle, + active_editor_index: Default::default(), + prev_active_editor_index: Default::default(), + editors: Default::default(), + saved_conversations, + saved_conversations_scroll_handle: Default::default(), + zoomed: false, + focus_handle, + toolbar, + completion_provider, + api_key_editor: None, + languages: workspace.app_state().languages.clone(), + fs: workspace.app_state().fs.clone(), + width: None, + height: None, + subscriptions: Default::default(), + next_inline_assist_id: 0, + pending_inline_assists: Default::default(), + pending_inline_assist_ids_by_editor: Default::default(), + include_conversation_in_next_inline_assist: false, + inline_prompt_history: Default::default(), + _watch_saved_conversations, + semantic_index, + retrieve_context_in_next_inline_assist: false, + }; + + let mut old_dock_position = this.position(cx); + this.subscriptions = + vec![cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(AssistantPanelEvent::DockPositionChanged); + } + cx.notify(); + })]; + + this + }) + }) + }) + } + + fn focus_in(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx)); + cx.notify(); + if self.focus_handle.is_focused(cx) { + if let Some(editor) = self.active_editor() { + cx.focus_view(editor); + } else if let Some(api_key_editor) = self.api_key_editor.as_ref() { + cx.focus_view(api_key_editor); + } + } + } + + fn focus_out(&mut self, cx: &mut ViewContext) { + self.toolbar + .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx)); + cx.notify(); + } + + pub fn inline_assist( + workspace: &mut Workspace, + _: &InlineAssist, + cx: &mut ViewContext, + ) { + let this = if let Some(this) = workspace.panel::(cx) { + if this.update(cx, |assistant, cx| { + if !assistant.has_credentials() { + assistant.load_credentials(cx); + }; + + assistant.has_credentials() + }) { + this + } else { + workspace.focus_panel::(cx); + return; + } + } else { + return; + }; + + let active_editor = if let Some(active_editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + { + active_editor + } else { + return; + }; + + let project = workspace.project(); + + this.update(cx, |assistant, cx| { + assistant.new_inline_assist(&active_editor, cx, project) + }); + } + + fn new_inline_assist( + &mut self, + editor: &View, + cx: &mut ViewContext, + project: &Model, + ) { + let selection = editor.read(cx).selections.newest_anchor().clone(); + if selection.start.excerpt_id != selection.end.excerpt_id { + return; + } + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + + // Extend the selection to the start and the end of the line. + let mut point_selection = selection.map(|selection| selection.to_point(&snapshot)); + if point_selection.end > point_selection.start { + point_selection.start.column = 0; + // If the selection ends at the start of the line, we don't want to include it. + if point_selection.end.column == 0 { + point_selection.end.row -= 1; + } + point_selection.end.column = snapshot.line_len(point_selection.end.row); + } + + let codegen_kind = if point_selection.start == point_selection.end { + CodegenKind::Generate { + position: snapshot.anchor_after(point_selection.start), + } + } else { + CodegenKind::Transform { + range: snapshot.anchor_before(point_selection.start) + ..snapshot.anchor_after(point_selection.end), + } + }; + + let inline_assist_id = post_inc(&mut self.next_inline_assist_id); + let provider = self.completion_provider.clone(); + + // Retrieve Credentials Authenticates the Provider + provider.retrieve_credentials(cx); + + let codegen = cx.add_model(|cx| { + Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) + }); + + if let Some(semantic_index) = self.semantic_index.clone() { + let project = project.clone(); + cx.spawn(|_, mut cx| async move { + let previously_indexed = semantic_index + .update(&mut cx, |index, cx| { + index.project_previously_indexed(&project, cx) + }) + .await + .unwrap_or(false); + if previously_indexed { + let _ = semantic_index + .update(&mut cx, |index, cx| { + index.index_project(project.clone(), cx) + }) + .await; + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + let measurements = Rc::new(Cell::new(BlockMeasurements::default())); + let inline_assistant = cx.add_view(|cx| { + let assistant = InlineAssistant::new( + inline_assist_id, + measurements.clone(), + self.include_conversation_in_next_inline_assist, + self.inline_prompt_history.clone(), + codegen.clone(), + self.workspace.clone(), + cx, + self.retrieve_context_in_next_inline_assist, + self.semantic_index.clone(), + project.clone(), + ); + cx.focus_self(); + assistant + }); + let block_id = editor.update(cx, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_anchor_ranges([selection.head()..selection.head()]) + }); + editor.insert_blocks( + [BlockProperties { + style: BlockStyle::Flex, + position: snapshot.anchor_before(point_selection.head()), + height: 2, + render: Arc::new({ + let inline_assistant = inline_assistant.clone(); + move |cx: &mut BlockContext| { + measurements.set(BlockMeasurements { + anchor_x: cx.anchor_x, + gutter_width: cx.gutter_width, + }); + inline_assistant.clone().into_any_element() + } + }), + disposition: if selection.reversed { + BlockDisposition::Above + } else { + BlockDisposition::Below + }, + }], + Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)), + cx, + )[0] + }); + + self.pending_inline_assists.insert( + inline_assist_id, + PendingInlineAssist { + editor: editor.downgrade(), + inline_assistant: Some((block_id, inline_assistant.clone())), + codegen: codegen.clone(), + project: project.downgrade(), + _subscriptions: vec![ + cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event), + cx.subscribe(editor, { + let inline_assistant = inline_assistant.downgrade(); + move |_, editor, event, cx| { + if let Some(inline_assistant) = inline_assistant.upgrade() { + if let EditorEvent::SelectionsChanged { local } = event { + if *local && inline_assistant.read(cx).has_focus { + cx.focus(&editor); + } + } + } + } + }), + cx.observe(&codegen, { + let editor = editor.downgrade(); + move |this, _, cx| { + if let Some(editor) = editor.upgrade() { + this.update_highlights_for_editor(&editor, cx); + } + } + }), + cx.subscribe(&codegen, move |this, codegen, event, cx| match event { + codegen::Event::Undone => { + this.finish_inline_assist(inline_assist_id, false, cx) + } + codegen::Event::Finished => { + let pending_assist = if let Some(pending_assist) = + this.pending_inline_assists.get(&inline_assist_id) + { + pending_assist + } else { + return; + }; + + let error = codegen + .read(cx) + .error() + .map(|error| format!("Inline assistant error: {}", error)); + if let Some(error) = error { + if pending_assist.inline_assistant.is_none() { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.show_toast( + Toast::new(inline_assist_id, error), + cx, + ); + }) + } + + this.finish_inline_assist(inline_assist_id, false, cx); + } + } else { + this.finish_inline_assist(inline_assist_id, false, cx); + } + } + }), + ], + }, + ); + self.pending_inline_assist_ids_by_editor + .entry(editor.downgrade()) + .or_default() + .push(inline_assist_id); + self.update_highlights_for_editor(&editor, cx); + } + + fn handle_inline_assistant_event( + &mut self, + inline_assistant: View, + event: &InlineAssistantEvent, + cx: &mut ViewContext, + ) { + let assist_id = inline_assistant.read(cx).id; + match event { + InlineAssistantEvent::Confirmed { + prompt, + include_conversation, + retrieve_context, + } => { + self.confirm_inline_assist( + assist_id, + prompt, + *include_conversation, + cx, + *retrieve_context, + ); + } + InlineAssistantEvent::Canceled => { + self.finish_inline_assist(assist_id, true, cx); + } + InlineAssistantEvent::Dismissed => { + self.hide_inline_assist(assist_id, cx); + } + InlineAssistantEvent::IncludeConversationToggled { + include_conversation, + } => { + self.include_conversation_in_next_inline_assist = *include_conversation; + } + InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => { + self.retrieve_context_in_next_inline_assist = *retrieve_context + } + } + } + + fn cancel_last_inline_assist( + workspace: &mut Workspace, + _: &editor::Cancel, + cx: &mut ViewContext, + ) { + if let Some(panel) = workspace.panel::(cx) { + if let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.downcast::()) + { + let handled = panel.update(cx, |panel, cx| { + if let Some(assist_id) = panel + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .and_then(|assist_ids| assist_ids.last().copied()) + { + panel.finish_inline_assist(assist_id, true, cx); + true + } else { + false + } + }); + if handled { + return; + } + } + } + + cx.propagate_action(); + } + + fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { + self.hide_inline_assist(assist_id, cx); + + if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { + if let hash_map::Entry::Occupied(mut entry) = self + .pending_inline_assist_ids_by_editor + .entry(pending_assist.editor) + { + entry.get_mut().retain(|id| *id != assist_id); + if entry.get().is_empty() { + entry.remove(); + } + } + + if let Some(editor) = pending_assist.editor.upgrade() { + self.update_highlights_for_editor(&editor, cx); + + if undo { + pending_assist + .codegen + .update(cx, |codegen, cx| codegen.undo(cx)); + } + } + } + } + + fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext) { + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) { + if let Some(editor) = pending_assist.editor.upgrade() { + if let Some((block_id, _)) = pending_assist.inline_assistant.take() { + editor.update(cx, |editor, cx| { + editor.remove_blocks(HashSet::from_iter([block_id]), None, cx); + }); + } + } + } + } + + fn confirm_inline_assist( + &mut self, + inline_assist_id: usize, + user_prompt: &str, + include_conversation: bool, + cx: &mut ViewContext, + retrieve_context: bool, + ) { + let conversation = if include_conversation { + self.active_editor() + .map(|editor| editor.read(cx).conversation.clone()) + } else { + None + }; + + let pending_assist = + if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) { + pending_assist + } else { + return; + }; + + let editor = if let Some(editor) = pending_assist.editor.upgrade() { + editor + } else { + return; + }; + + let project = pending_assist.project.clone(); + + let project_name = if let Some(project) = project.upgrade() { + Some( + project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"), + ) + } else { + None + }; + + self.inline_prompt_history + .retain(|prompt| prompt != user_prompt); + self.inline_prompt_history.push_back(user_prompt.into()); + if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN { + self.inline_prompt_history.pop_front(); + } + + let codegen = pending_assist.codegen.clone(); + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + let range = codegen.read(cx).range(); + let start = snapshot.point_to_buffer_offset(range.start); + let end = snapshot.point_to_buffer_offset(range.end); + let (buffer, range) = if let Some((start, end)) = start.zip(end) { + let (start_buffer, start_buffer_offset) = start; + let (end_buffer, end_buffer_offset) = end; + if start_buffer.remote_id() == end_buffer.remote_id() { + (start_buffer.clone(), start_buffer_offset..end_buffer_offset) + } else { + self.finish_inline_assist(inline_assist_id, false, cx); + return; + } + } else { + self.finish_inline_assist(inline_assist_id, false, cx); + return; + }; + + let language = buffer.language_at(range.start); + let language_name = if let Some(language) = language.as_ref() { + if Arc::ptr_eq(language, &language::PLAIN_TEXT) { + None + } else { + Some(language.name()) + } + } else { + None + }; + + // Higher Temperature increases the randomness of model outputs. + // If Markdown or No Language is Known, increase the randomness for more creative output + // If Code, decrease temperature to get more deterministic outputs + let temperature = if let Some(language) = language_name.clone() { + if language.to_string() != "Markdown".to_string() { + 0.5 + } else { + 1.0 + } + } else { + 1.0 + }; + + let user_prompt = user_prompt.to_string(); + + let snippets = if retrieve_context { + let Some(project) = project.upgrade() else { + return; + }; + + let search_results = if let Some(semantic_index) = self.semantic_index.clone() { + let search_results = semantic_index.update(cx, |this, cx| { + this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx) + }); + + cx.background_executor() + .spawn(async move { search_results.await.unwrap_or_default() }) + } else { + Task::ready(Vec::new()) + }; + + let snippets = cx.spawn(|_, mut cx| async move { + let mut snippets = Vec::new(); + for result in search_results.await { + snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx)); + } + snippets + }); + snippets + } else { + Task::ready(Vec::new()) + }; + + let mut model = AssistantSettings::get_global(cx) + .default_open_ai_model + .clone(); + let model_name = model.full_name(); + + let prompt = cx.background_executor().spawn(async move { + let snippets = snippets.await; + + let language_name = language_name.as_deref(); + generate_content_prompt( + user_prompt, + language_name, + buffer, + range, + snippets, + model_name, + project_name, + ) + }); + + let mut messages = Vec::new(); + if let Some(conversation) = conversation { + let conversation = conversation.read(cx); + let buffer = conversation.buffer.read(cx); + messages.extend( + conversation + .messages(cx) + .map(|message| message.to_open_ai_message(buffer)), + ); + model = conversation.model.clone(); + } + + cx.spawn(|_, mut cx| async move { + // I Don't know if we want to return a ? here. + let prompt = prompt.await?; + + messages.push(RequestMessage { + role: Role::User, + content: prompt, + }); + + let request = Box::new(OpenAIRequest { + model: model.full_name().into(), + messages, + stream: true, + stop: vec!["|END|>".to_string()], + temperature, + }); + + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + anyhow::Ok(()) + }) + .detach(); + } + + fn update_highlights_for_editor(&self, editor: &View, cx: &mut ViewContext) { + let mut background_ranges = Vec::new(); + let mut foreground_ranges = Vec::new(); + let empty_inline_assist_ids = Vec::new(); + let inline_assist_ids = self + .pending_inline_assist_ids_by_editor + .get(&editor.downgrade()) + .unwrap_or(&empty_inline_assist_ids); + + for inline_assist_id in inline_assist_ids { + if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) { + let codegen = pending_assist.codegen.read(cx); + background_ranges.push(codegen.range()); + foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned()); + } + } + + let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx); + merge_ranges(&mut background_ranges, &snapshot); + merge_ranges(&mut foreground_ranges, &snapshot); + editor.update(cx, |editor, cx| { + if background_ranges.is_empty() { + editor.clear_background_highlights::(cx); + } else { + editor.highlight_background::( + background_ranges, + |theme| theme.assistant.inline.pending_edit_background, + cx, + ); + } + + if foreground_ranges.is_empty() { + editor.clear_highlights::(cx); + } else { + editor.highlight_text::( + foreground_ranges, + HighlightStyle { + fade_out: Some(0.6), + ..Default::default() + }, + cx, + ); + } + }); + } + + fn new_conversation(&mut self, cx: &mut ViewContext) -> View { + let editor = cx.add_view(|cx| { + ConversationEditor::new( + self.completion_provider.clone(), + self.languages.clone(), + self.fs.clone(), + self.workspace.clone(), + cx, + ) + }); + self.add_conversation(editor.clone(), cx); + editor + } + + fn add_conversation(&mut self, editor: View, cx: &mut ViewContext) { + self.subscriptions + .push(cx.subscribe(&editor, Self::handle_conversation_editor_event)); + + let conversation = editor.read(cx).conversation.clone(); + self.subscriptions + .push(cx.observe(&conversation, |_, _, cx| cx.notify())); + + let index = self.editors.len(); + self.editors.push(editor); + self.set_active_editor_index(Some(index), cx); + } + + fn set_active_editor_index(&mut self, index: Option, cx: &mut ViewContext) { + self.prev_active_editor_index = self.active_editor_index; + self.active_editor_index = index; + if let Some(editor) = self.active_editor() { + let editor = editor.read(cx).editor.clone(); + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(Some(&editor), cx); + }); + if self.has_focus(cx) { + cx.focus(&editor); + } + } else { + self.toolbar.update(cx, |toolbar, cx| { + toolbar.set_active_item(None, cx); + }); + } + + cx.notify(); + } + + fn handle_conversation_editor_event( + &mut self, + _: View, + event: &ConversationEditorEvent, + cx: &mut ViewContext, + ) { + match event { + ConversationEditorEvent::TabContentChanged => cx.notify(), + } + } + + fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if let Some(api_key) = self + .api_key_editor + .as_ref() + .map(|editor| editor.read(cx).text(cx)) + { + if !api_key.is_empty() { + let credential = ProviderCredential::Credentials { + api_key: api_key.clone(), + }; + + self.completion_provider.save_credentials(cx, credential); + + self.api_key_editor.take(); + cx.focus_self(); + cx.notify(); + } + } else { + cx.propagate_action(); + } + } + + fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { + self.completion_provider.delete_credentials(cx); + self.api_key_editor = Some(build_api_key_editor(cx)); + cx.focus_self(); + cx.notify(); + } + + fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { + if self.zoomed { + cx.emit(AssistantPanelEvent::ZoomOut) + } else { + cx.emit(AssistantPanelEvent::ZoomIn) + } + } + + fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { + let mut propagate_action = true; + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |search_bar, cx| { + if search_bar.show(cx) { + search_bar.search_suggested(cx); + if action.focus { + search_bar.select_query(cx); + cx.focus_self(); + } + propagate_action = false + } + }); + } + if propagate_action { + cx.propagate_action(); + } + } + + fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + if !search_bar.read(cx).is_dismissed() { + search_bar.update(cx, |search_bar, cx| { + search_bar.dismiss(&Default::default(), cx) + }); + return; + } + } + cx.propagate_action(); + } + + fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx)); + } + } + + fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext) { + if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { + search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx)); + } + } + + fn active_editor(&self) -> Option<&View> { + self.editors.get(self.active_editor_index?) + } + + fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("hamburger_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if this.active_editor().is_some() { + this.set_active_editor_index(None, cx); + } else { + this.set_active_editor_index(this.prev_active_editor_index, cx); + } + })) + .tooltip(|cx| ui::Tooltip::text("History", cx)) + } + + fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { + if self.active_editor().is_some() { + vec![ + Self::render_split_button(cx).into_any(), + Self::render_quote_button(cx).into_any(), + Self::render_assist_button(cx).into_any(), + ] + } else { + Default::default() + } + } + + fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("split_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); + } + })) + .tooltip(|cx| ui::Tooltip::for_action("Split Message", &Split, cx)) + } + + fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("assist_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if let Some(active_editor) = this.active_editor() { + active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); + } + })) + .tooltip(|cx| ui::Tooltip::for_action("Assist", &Assist, cx)) + } + + fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("quote_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + if let Some(workspace) = this.workspace.upgrade() { + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + ConversationEditor::quote_selection(workspace, &Default::default(), cx) + }); + }); + } + })) + .tooltip(|cx| ui::Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) + } + + fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("plus_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + this.new_conversation(cx); + })) + .tooltip(|cx| ui::Tooltip::for_action("New Conversation", &NewConversation, cx)) + } + + fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { + IconButton::new("zoom_button", ui::Icon::Menu) + .on_click(cx.listener(|this, _event, cx| { + this.toggle_zoom(&ToggleZoom, cx); + })) + .tooltip(|cx| { + ui::Tooltip::for_action( + if self.zoomed { "Zoom Out" } else { "Zoom In" }, + &ToggleZoom, + cx, + ) + }) + } + + fn render_saved_conversation( + &mut self, + index: usize, + cx: &mut ViewContext, + ) -> impl IntoElement { + let conversation = &self.saved_conversations[index]; + let path = conversation.path.clone(); + + ButtonLike::new(index) + .on_click(cx.listener(move |this, _, cx| { + this.open_conversation(path.clone(), cx) + .detach_and_log_err(cx) + })) + .child(Label::new( + conversation.mtime.format("%F %I:%M%p").to_string(), + )) + .child(Label::new(conversation.title.clone())) + } + + fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext) -> Task> { + if let Some(ix) = self.editor_index_for_path(&path, cx) { + self.set_active_editor_index(Some(ix), cx); + return Task::ready(Ok(())); + } + + let fs = self.fs.clone(); + let workspace = self.workspace.clone(); + let languages = self.languages.clone(); + cx.spawn(|this, mut cx| async move { + let saved_conversation = fs.load(&path).await?; + let saved_conversation = serde_json::from_str(&saved_conversation)?; + let conversation = cx.add_model(|cx| { + Conversation::deserialize(saved_conversation, path.clone(), languages, cx) + }); + this.update(&mut cx, |this, cx| { + // If, by the time we've loaded the conversation, the user has already opened + // the same conversation, we don't want to open it again. + if let Some(ix) = this.editor_index_for_path(&path, cx) { + this.set_active_editor_index(Some(ix), cx); + } else { + let editor = cx.add_view(|cx| { + ConversationEditor::for_conversation(conversation, fs, workspace, cx) + }); + this.add_conversation(editor, cx); + } + })?; + Ok(()) + }) + } + + fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option { + self.editors + .iter() + .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path)) + } + + fn has_credentials(&mut self) -> bool { + self.completion_provider.has_credentials() + } + + fn load_credentials(&mut self, cx: &mut ViewContext) { + self.completion_provider.retrieve_credentials(cx); + } +} + +fn build_api_key_editor(cx: &mut ViewContext) -> View { + cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); + editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx); + editor + }) +} + +impl Render for AssistantPanel { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(api_key_editor) = self.api_key_editor.clone() { + v_stack() + .track_focus(&self.focus_handle) + .child(Label::new( + "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", + )) + .child(Label::new( + " - Having a subscription for another service like GitHub Copilot won't work." + )) + .child(Label::new( + " - You can create a api key at: platform.openai.com/api-keys" + )) + .child(Label::new( + " " + )) + .child(Label::new( + "Paste your OpenAI API key and press Enter to use the assistant" + )) + .child(api_key_editor) + .child(Label::new( + "Click on the Z button in the status bar to close this panel." + )) + .border() + .border_color(gpui::red()) + } else { + let title = self + .active_editor() + .map(|editor| Label::new(editor.read(cx).title(cx))); + + let mut header = h_stack() + .child(Self::render_hamburger_button(cx)) + .children(title); + + if self.focus_handle.contains_focused(cx) { + header = header + .children(self.render_editor_tools(cx)) + .child(Self::render_plus_button(cx)) + .child(self.render_zoom_button(cx)); + } + + v_stack() + .track_focus(&self.focus_handle) + .child(header) + .children(if self.toolbar.read(cx).hidden() { + None + } else { + Some(self.toolbar.clone()) + }) + .child(if let Some(editor) = self.active_editor() { + editor.clone().into_any_element() + } else { + uniform_list( + cx.view().clone(), + "saved_conversations", + self.saved_conversations.len(), + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx).into_any()) + .collect() + }, + ) + .track_scroll(self.saved_conversations_scroll_handle.clone()) + .into_any_element() + }) + .border() + .border_color(gpui::red()) + } + } +} + +impl Panel for AssistantPanel { + fn persistent_name() -> &'static str { + "AssistantPanel" + } + + fn position(&self, cx: &WindowContext) -> DockPosition { + match AssistantSettings::get_global(cx).dock { + AssistantDockPosition::Left => DockPosition::Left, + AssistantDockPosition::Bottom => DockPosition::Bottom, + AssistantDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, _: DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::(self.fs.clone(), cx, move |settings| { + let dock = match position { + DockPosition::Left => AssistantDockPosition::Left, + DockPosition::Bottom => AssistantDockPosition::Bottom, + DockPosition::Right => AssistantDockPosition::Right, + }; + settings.dock = Some(dock); + }); + } + + fn size(&self, cx: &WindowContext) -> f32 { + let settings = AssistantSettings::get_global(cx); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or_else(|| settings.default_width) + } + DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), + } + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = size, + DockPosition::Bottom => self.height = size, + } + cx.notify(); + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.zoomed = zoomed; + cx.notify(); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + if active { + self.load_credentials(cx); + + if self.editors.is_empty() { + self.new_conversation(cx); + } + } + } + + fn icon(&self, cx: &WindowContext) -> Option { + Some(ui::Icon::Ai) + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } +} + +impl EventEmitter for AssistantPanel {} + +impl FocusableView for AssistantPanel { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +enum ConversationEvent { + MessagesEdited, + SummaryChanged, + StreamedCompletion, +} + +#[derive(Default)] +struct Summary { + text: String, + done: bool, +} + +struct Conversation { + id: Option, + buffer: Model, + message_anchors: Vec, + messages_metadata: HashMap, + next_message_id: MessageId, + summary: Option, + pending_summary: Task>, + completion_count: usize, + pending_completions: Vec, + model: OpenAIModel, + token_count: Option, + max_token_count: usize, + pending_token_count: Task>, + pending_save: Task>, + path: Option, + _subscriptions: Vec, + completion_provider: Arc, +} + +impl EventEmitter for Conversation {} + +impl Conversation { + fn new( + language_registry: Arc, + cx: &mut ModelContext, + completion_provider: Arc, + ) -> Self { + let markdown = language_registry.language_for_name("Markdown"); + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); + buffer.set_language_registry(language_registry); + cx.spawn_weak(|buffer, mut cx| async move { + let markdown = markdown.await?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + buffer + }); + + let settings = AssistantSettings::get_global(cx); + let model = settings.default_open_ai_model.clone(); + + let mut this = Self { + id: Some(Uuid::new_v4().to_string()), + message_anchors: Default::default(), + messages_metadata: Default::default(), + next_message_id: Default::default(), + summary: None, + pending_summary: Task::ready(None), + completion_count: Default::default(), + pending_completions: Default::default(), + token_count: None, + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), + pending_token_count: Task::ready(None), + model: model.clone(), + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + pending_save: Task::ready(Ok(())), + path: None, + buffer, + completion_provider, + }; + let message = MessageAnchor { + id: MessageId(post_inc(&mut this.next_message_id.0)), + start: language::Anchor::MIN, + }; + this.message_anchors.push(message.clone()); + this.messages_metadata.insert( + message.id, + MessageMetadata { + role: Role::User, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + + this.count_remaining_tokens(cx); + this + } + + fn serialize(&self, cx: &AppContext) -> SavedConversation { + SavedConversation { + id: self.id.clone(), + zed: "conversation".into(), + version: SavedConversation::VERSION.into(), + text: self.buffer.read(cx).text(), + message_metadata: self.messages_metadata.clone(), + messages: self + .messages(cx) + .map(|message| SavedMessage { + id: message.id, + start: message.offset_range.start, + }) + .collect(), + summary: self + .summary + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_default(), + model: self.model.clone(), + } + } + + fn deserialize( + saved_conversation: SavedConversation, + path: PathBuf, + language_registry: Arc, + cx: &mut ModelContext, + ) -> Self { + let id = match saved_conversation.id { + Some(id) => Some(id), + None => Some(Uuid::new_v4().to_string()), + }; + let model = saved_conversation.model; + let completion_provider: Arc = Arc::new( + OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()), + ); + completion_provider.retrieve_credentials(cx); + let markdown = language_registry.language_for_name("Markdown"); + let mut message_anchors = Vec::new(); + let mut next_message_id = MessageId(0); + let buffer = cx.add_model(|cx| { + let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); + for message in saved_conversation.messages { + message_anchors.push(MessageAnchor { + id: message.id, + start: buffer.anchor_before(message.start), + }); + next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); + } + buffer.set_language_registry(language_registry); + cx.spawn_weak(|buffer, mut cx| async move { + let markdown = markdown.await?; + let buffer = buffer + .upgrade(&cx) + .ok_or_else(|| anyhow!("buffer was dropped"))?; + buffer.update(&mut cx, |buffer: &mut Buffer, cx| { + buffer.set_language(Some(markdown), cx) + }); + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + buffer + }); + + let mut this = Self { + id, + message_anchors, + messages_metadata: saved_conversation.message_metadata, + next_message_id, + summary: Some(Summary { + text: saved_conversation.summary, + done: true, + }), + pending_summary: Task::ready(None), + completion_count: Default::default(), + pending_completions: Default::default(), + token_count: None, + max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()), + pending_token_count: Task::ready(None), + model, + _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)], + pending_save: Task::ready(Ok(())), + path: Some(path), + buffer, + completion_provider, + }; + this.count_remaining_tokens(cx); + this + } + + fn handle_buffer_event( + &mut self, + _: Model, + event: &language::Event, + cx: &mut ModelContext, + ) { + match event { + language::Event::Edited => { + self.count_remaining_tokens(cx); + cx.emit(ConversationEvent::MessagesEdited); + } + _ => {} + } + } + + fn count_remaining_tokens(&mut self, cx: &mut ModelContext) { + let messages = self + .messages(cx) + .into_iter() + .filter_map(|message| { + Some(tiktoken_rs::ChatCompletionRequestMessage { + role: match message.role { + Role::User => "user".into(), + Role::Assistant => "assistant".into(), + Role::System => "system".into(), + }, + content: Some( + self.buffer + .read(cx) + .text_for_range(message.offset_range) + .collect(), + ), + name: None, + function_call: None, + }) + }) + .collect::>(); + let model = self.model.clone(); + self.pending_token_count = cx.spawn_weak(|this, mut cx| { + async move { + cx.background_executor() + .timer(Duration::from_millis(200)) + .await; + let token_count = cx + .background() + .spawn(async move { + tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) + }) + .await?; + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); + this.token_count = Some(token_count); + cx.notify() + }); + anyhow::Ok(()) + } + .log_err() + }); + } + + fn remaining_tokens(&self) -> Option { + Some(self.max_token_count as isize - self.token_count? as isize) + } + + fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext) { + self.model = model; + self.count_remaining_tokens(cx); + cx.notify(); + } + + fn assist( + &mut self, + selected_messages: HashSet, + cx: &mut ModelContext, + ) -> Vec { + let mut user_messages = Vec::new(); + + let last_message_id = if let Some(last_message_id) = + self.message_anchors.iter().rev().find_map(|message| { + message + .start + .is_valid(self.buffer.read(cx)) + .then_some(message.id) + }) { + last_message_id + } else { + return Default::default(); + }; + + let mut should_assist = false; + for selected_message_id in selected_messages { + let selected_message_role = + if let Some(metadata) = self.messages_metadata.get(&selected_message_id) { + metadata.role + } else { + continue; + }; + + if selected_message_role == Role::Assistant { + if let Some(user_message) = self.insert_message_after( + selected_message_id, + Role::User, + MessageStatus::Done, + cx, + ) { + user_messages.push(user_message); + } + } else { + should_assist = true; + } + } + + if should_assist { + if !self.completion_provider.has_credentials() { + return Default::default(); + } + + let request: Box = Box::new(OpenAIRequest { + model: self.model.full_name().to_string(), + messages: self + .messages(cx) + .filter(|message| matches!(message.status, MessageStatus::Done)) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .collect(), + stream: true, + stop: vec![], + temperature: 1.0, + }); + + let stream = self.completion_provider.complete(request); + let assistant_message = self + .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx) + .unwrap(); + + // Queue up the user's next reply. + let user_message = self + .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx) + .unwrap(); + user_messages.push(user_message); + + let task = cx.spawn_weak({ + |this, mut cx| async move { + let assistant_message_id = assistant_message.id; + let stream_completion = async { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let text = message?; + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] + .iter() + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); + }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + }); + smol::future::yield_now().await; + } + + this.upgrade(&cx) + .ok_or_else(|| anyhow!("conversation was dropped"))? + .update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + }); + + anyhow::Ok(()) + }; + + let result = stream_completion.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); + } + } + cx.notify(); + } + }); + } + } + }); + + self.pending_completions.push(PendingCompletion { + id: post_inc(&mut self.completion_count), + _task: task, + }); + } + + user_messages + } + + fn cancel_last_assist(&mut self) -> bool { + self.pending_completions.pop().is_some() + } + + fn cycle_message_roles(&mut self, ids: HashSet, cx: &mut ModelContext) { + for id in ids { + if let Some(metadata) = self.messages_metadata.get_mut(&id) { + metadata.role.cycle(); + cx.emit(ConversationEvent::MessagesEdited); + cx.notify(); + } + } + } + + fn insert_message_after( + &mut self, + message_id: MessageId, + role: Role, + status: MessageStatus, + cx: &mut ModelContext, + ) -> Option { + if let Some(prev_message_ix) = self + .message_anchors + .iter() + .position(|message| message.id == message_id) + { + // Find the next valid message after the one we were given. + let mut next_message_ix = prev_message_ix + 1; + while let Some(next_message) = self.message_anchors.get(next_message_ix) { + if next_message.start.is_valid(self.buffer.read(cx)) { + break; + } + next_message_ix += 1; + } + + let start = self.buffer.update(cx, |buffer, cx| { + let offset = self + .message_anchors + .get(next_message_ix) + .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1); + buffer.edit([(offset..offset, "\n")], None, cx); + buffer.anchor_before(offset + 1) + }); + let message = MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start, + }; + self.message_anchors + .insert(next_message_ix, message.clone()); + self.messages_metadata.insert( + message.id, + MessageMetadata { + role, + sent_at: Local::now(), + status, + }, + ); + cx.emit(ConversationEvent::MessagesEdited); + Some(message) + } else { + None + } + } + + fn split_message( + &mut self, + range: Range, + cx: &mut ModelContext, + ) -> (Option, Option) { + let start_message = self.message_for_offset(range.start, cx); + let end_message = self.message_for_offset(range.end, cx); + if let Some((start_message, end_message)) = start_message.zip(end_message) { + // Prevent splitting when range spans multiple messages. + if start_message.id != end_message.id { + return (None, None); + } + + let message = start_message; + let role = message.role; + let mut edited_buffer = false; + + let mut suffix_start = None; + if range.start > message.offset_range.start && range.end < message.offset_range.end - 1 + { + if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') { + suffix_start = Some(range.end + 1); + } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') { + suffix_start = Some(range.end); + } + } + + let suffix = if let Some(suffix_start) = suffix_start { + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(suffix_start), + } + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(range.end..range.end, "\n")], None, cx); + }); + edited_buffer = true; + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(range.end + 1), + } + }; + + self.message_anchors + .insert(message.index_range.end + 1, suffix.clone()); + self.messages_metadata.insert( + suffix.id, + MessageMetadata { + role, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + + let new_messages = + if range.start == range.end || range.start == message.offset_range.start { + (None, Some(suffix)) + } else { + let mut prefix_end = None; + if range.start > message.offset_range.start + && range.end < message.offset_range.end - 1 + { + if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') { + prefix_end = Some(range.start + 1); + } else if self.buffer.read(cx).reversed_chars_at(range.start).next() + == Some('\n') + { + prefix_end = Some(range.start); + } + } + + let selection = if let Some(prefix_end) = prefix_end { + cx.emit(ConversationEvent::MessagesEdited); + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(prefix_end), + } + } else { + self.buffer.update(cx, |buffer, cx| { + buffer.edit([(range.start..range.start, "\n")], None, cx) + }); + edited_buffer = true; + MessageAnchor { + id: MessageId(post_inc(&mut self.next_message_id.0)), + start: self.buffer.read(cx).anchor_before(range.end + 1), + } + }; + + self.message_anchors + .insert(message.index_range.end + 1, selection.clone()); + self.messages_metadata.insert( + selection.id, + MessageMetadata { + role, + sent_at: Local::now(), + status: MessageStatus::Done, + }, + ); + (Some(selection), Some(suffix)) + }; + + if !edited_buffer { + cx.emit(ConversationEvent::MessagesEdited); + } + new_messages + } else { + (None, None) + } + } + + fn summarize(&mut self, cx: &mut ModelContext) { + if self.message_anchors.len() >= 2 && self.summary.is_none() { + if !self.completion_provider.has_credentials() { + return; + } + + let messages = self + .messages(cx) + .take(2) + .map(|message| message.to_open_ai_message(self.buffer.read(cx))) + .chain(Some(RequestMessage { + role: Role::User, + content: "Summarize the conversation into a short title without punctuation" + .into(), + })); + let request: Box = Box::new(OpenAIRequest { + model: self.model.full_name().to_string(), + messages: messages.collect(), + stream: true, + stop: vec![], + temperature: 1.0, + }); + + let stream = self.completion_provider.complete(request); + self.pending_summary = cx.spawn(|this, mut cx| { + async move { + let mut messages = stream.await?; + + while let Some(message) = messages.next().await { + let text = message?; + this.update(&mut cx, |this, cx| { + this.summary + .get_or_insert(Default::default()) + .text + .push_str(&text); + cx.emit(ConversationEvent::SummaryChanged); + }); + } + + this.update(&mut cx, |this, cx| { + if let Some(summary) = this.summary.as_mut() { + summary.done = true; + cx.emit(ConversationEvent::SummaryChanged); + } + }); + + anyhow::Ok(()) + } + .log_err() + }); + } + } + + fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option { + self.messages_for_offsets([offset], cx).pop() + } + + fn messages_for_offsets( + &self, + offsets: impl IntoIterator, + cx: &AppContext, + ) -> Vec { + let mut result = Vec::new(); + + let mut messages = self.messages(cx).peekable(); + let mut offsets = offsets.into_iter().peekable(); + let mut current_message = messages.next(); + while let Some(offset) = offsets.next() { + // Locate the message that contains the offset. + while current_message.as_ref().map_or(false, |message| { + !message.offset_range.contains(&offset) && messages.peek().is_some() + }) { + current_message = messages.next(); + } + let Some(message) = current_message.as_ref() else { + break; + }; + + // Skip offsets that are in the same message. + while offsets.peek().map_or(false, |offset| { + message.offset_range.contains(offset) || messages.peek().is_none() + }) { + offsets.next(); + } + + result.push(message.clone()); + } + result + } + + fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator { + let buffer = self.buffer.read(cx); + let mut message_anchors = self.message_anchors.iter().enumerate().peekable(); + iter::from_fn(move || { + while let Some((start_ix, message_anchor)) = message_anchors.next() { + let metadata = self.messages_metadata.get(&message_anchor.id)?; + let message_start = message_anchor.start.to_offset(buffer); + let mut message_end = None; + let mut end_ix = start_ix; + while let Some((_, next_message)) = message_anchors.peek() { + if next_message.start.is_valid(buffer) { + message_end = Some(next_message.start); + break; + } else { + end_ix += 1; + message_anchors.next(); + } + } + let message_end = message_end + .unwrap_or(language::Anchor::MAX) + .to_offset(buffer); + return Some(Message { + index_range: start_ix..end_ix, + offset_range: message_start..message_end, + id: message_anchor.id, + anchor: message_anchor.start, + role: metadata.role, + sent_at: metadata.sent_at, + status: metadata.status.clone(), + }); + } + None + }) + } + + fn save( + &mut self, + debounce: Option, + fs: Arc, + cx: &mut ModelContext, + ) { + self.pending_save = cx.spawn(|this, mut cx| async move { + if let Some(debounce) = debounce { + cx.background_executor().timer(debounce).await; + } + + let (old_path, summary) = this.read_with(&cx, |this, _| { + let path = this.path.clone(); + let summary = if let Some(summary) = this.summary.as_ref() { + if summary.done { + Some(summary.text.clone()) + } else { + None + } + } else { + None + }; + (path, summary) + }); + + if let Some(summary) = summary { + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let path = if let Some(old_path) = old_path { + old_path + } else { + let mut discriminant = 1; + let mut new_path; + loop { + new_path = CONVERSATIONS_DIR.join(&format!( + "{} - {}.zed.json", + summary.trim(), + discriminant + )); + if fs.is_file(&new_path).await { + discriminant += 1; + } else { + break; + } + } + new_path + }; + + fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; + fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) + .await?; + this.update(&mut cx, |this, _| this.path = Some(path)); + } + + Ok(()) + }); + } +} + +struct PendingCompletion { + id: usize, + _task: Task<()>, +} + +enum ConversationEditorEvent { + TabContentChanged, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +struct ScrollPosition { + offset_before_cursor: gpui::Point, + cursor: Anchor, +} + +struct ConversationEditor { + conversation: Model, + fs: Arc, + workspace: WeakView, + editor: View, + blocks: HashSet, + scroll_position: Option, + _subscriptions: Vec, +} + +impl ConversationEditor { + fn new( + completion_provider: Arc, + language_registry: Arc, + fs: Arc, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let conversation = + cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider)); + Self::for_conversation(conversation, fs, workspace, cx) + } + + fn for_conversation( + conversation: Model, + fs: Arc, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let editor = cx.add_view(|cx| { + 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 + }); + + let _subscriptions = vec![ + cx.observe(&conversation, |_, _, cx| cx.notify()), + cx.subscribe(&conversation, Self::handle_conversation_event), + cx.subscribe(&editor, Self::handle_editor_event), + ]; + + let mut this = Self { + conversation, + editor, + blocks: Default::default(), + scroll_position: None, + fs, + workspace, + _subscriptions, + }; + this.update_message_headers(cx); + this + } + + fn assist(&mut self, _: &Assist, cx: &mut ViewContext) { + report_assistant_event( + self.workspace.clone(), + self.conversation.read(cx).id.clone(), + AssistantKind::Panel, + cx, + ); + + let cursors = self.cursors(cx); + + let user_messages = self.conversation.update(cx, |conversation, cx| { + let selected_messages = conversation + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + conversation.assist(selected_messages, cx) + }); + let new_selections = user_messages + .iter() + .map(|message| { + let cursor = message + .start + .to_offset(self.conversation.read(cx).buffer.read(cx)); + cursor..cursor + }) + .collect::>(); + if !new_selections.is_empty() { + self.editor.update(cx, |editor, cx| { + editor.change_selections( + Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)), + cx, + |selections| selections.select_ranges(new_selections), + ); + }); + // Avoid scrolling to the new cursor position so the assistant's output is stable. + cx.defer(|this, _| this.scroll_position = None); + } + } + + fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + if !self + .conversation + .update(cx, |conversation, _| conversation.cancel_last_assist()) + { + cx.propagate_action(); + } + } + + fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext) { + let cursors = self.cursors(cx); + self.conversation.update(cx, |conversation, cx| { + let messages = conversation + .messages_for_offsets(cursors, cx) + .into_iter() + .map(|message| message.id) + .collect(); + conversation.cycle_message_roles(messages, cx) + }); + } + + fn cursors(&self, cx: &AppContext) -> Vec { + let selections = self.editor.read(cx).selections.all::(cx); + selections + .into_iter() + .map(|selection| selection.head()) + .collect() + } + + fn handle_conversation_event( + &mut self, + _: Model, + event: &ConversationEvent, + cx: &mut ViewContext, + ) { + match event { + ConversationEvent::MessagesEdited => { + self.update_message_headers(cx); + self.conversation.update(cx, |conversation, cx| { + conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx); + }); + } + ConversationEvent::SummaryChanged => { + cx.emit(ConversationEditorEvent::TabContentChanged); + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx); + }); + } + ConversationEvent::StreamedCompletion => { + self.editor.update(cx, |editor, cx| { + if let Some(scroll_position) = self.scroll_position { + let snapshot = editor.snapshot(cx); + let cursor_point = scroll_position.cursor.to_display_point(&snapshot); + let scroll_top = + cursor_point.row() as f32 - scroll_position.offset_before_cursor.y; + editor.set_scroll_position( + point(scroll_position.offset_before_cursor.x, scroll_top), + cx, + ); + } + }); + } + } + } + + fn handle_editor_event( + &mut self, + _: View, + event: &EditorEvent, + cx: &mut ViewContext, + ) { + match event { + EditorEvent::ScrollPositionChanged { autoscroll, .. } => { + let cursor_scroll_position = self.cursor_scroll_position(cx); + if *autoscroll { + self.scroll_position = cursor_scroll_position; + } else if self.scroll_position != cursor_scroll_position { + self.scroll_position = None; + } + } + EditorEvent::SelectionsChanged { .. } => { + self.scroll_position = self.cursor_scroll_position(cx); + } + _ => {} + } + } + + fn cursor_scroll_position(&self, cx: &mut ViewContext) -> Option { + self.editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + let cursor = editor.selections.newest_anchor().head(); + let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32; + let scroll_position = editor + .scroll_manager + .anchor() + .scroll_position(&snapshot.display_snapshot); + + let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y()..scroll_bottom).contains(&cursor_row) { + Some(ScrollPosition { + cursor, + offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), + }) + } else { + None + } + }) + } + + fn update_message_headers(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + let buffer = editor.buffer().read(cx).snapshot(cx); + let excerpt_id = *buffer.as_singleton().unwrap().0; + let old_blocks = std::mem::take(&mut self.blocks); + let new_blocks = self + .conversation + .read(cx) + .messages(cx) + .map(|message| BlockProperties { + position: buffer.anchor_in_excerpt(excerpt_id, message.anchor), + height: 2, + style: BlockStyle::Sticky, + render: Arc::new({ + let conversation = self.conversation.clone(); + // let metadata = message.metadata.clone(); + // let message = message.clone(); + move |cx| { + enum Sender {} + enum ErrorTooltip {} + + let message_id = message.id; + let sender = MouseEventHandler::new::( + message_id.0, + cx, + |state, _| match message.role { + Role::User => { + let style = style.user_sender.style_for(state); + Label::new("You", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::Assistant => { + let style = style.assistant_sender.style_for(state); + Label::new("Assistant", style.text.clone()) + .contained() + .with_style(style.container) + } + Role::System => { + let style = style.system_sender.style_for(state); + Label::new("System", style.text.clone()) + .contained() + .with_style(style.container) + } + }, + ) + .with_cursor_style(CursorStyle::PointingHand) + .on_down(MouseButton::Left, { + let conversation = conversation.clone(); + move |_, _, cx| { + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) + } + }); + + Flex::row() + .with_child(sender.aligned()) + .with_child( + Label::new( + message.sent_at.format("%I:%M%P").to_string(), + style.sent_at.text.clone(), + ) + .contained() + .with_style(style.sent_at.container) + .aligned(), + ) + .with_children( + if let MessageStatus::Error(error) = &message.status { + Some( + Svg::new("icons/error.svg") + .with_color(style.error_icon.color) + .constrained() + .with_width(style.error_icon.width) + .contained() + .with_style(style.error_icon.container) + .with_tooltip::( + message_id.0, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }, + ) + .aligned() + .left() + .contained() + .with_style(style.message_header) + .into_any() + } + }), + disposition: BlockDisposition::Above, + }) + .collect::>(); + + editor.remove_blocks(old_blocks, None, cx); + let ids = editor.insert_blocks(new_blocks, None, cx); + self.blocks = HashSet::from_iter(ids); + }); + } + + fn quote_selection( + workspace: &mut Workspace, + _: &QuoteSelection, + cx: &mut ViewContext, + ) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + let Some(editor) = workspace + .active_item(cx) + .and_then(|item| item.act_as::(cx)) + else { + return; + }; + + let text = editor.read_with(cx, |editor, cx| { + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + + let selected_text = buffer.text_for_range(range).collect::(); + if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") + } else { + format!("```{language_name}\n{selected_text}\n```") + }) + } + }); + + // Activate the panel + if !panel.read(cx).has_focus(cx) { + workspace.toggle_panel_focus::(cx); + } + + if let Some(text) = text { + panel.update(cx, |panel, cx| { + let conversation = panel + .active_editor() + .cloned() + .unwrap_or_else(|| panel.new_conversation(cx)); + conversation.update(cx, |conversation, cx| { + conversation + .editor + .update(cx, |editor, cx| editor.insert(&text, cx)) + }); + }); + } + } + + fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext) { + let editor = self.editor.read(cx); + let conversation = self.conversation.read(cx); + if editor.selections.count() == 1 { + let selection = editor.selections.newest::(cx); + let mut copied_text = String::new(); + let mut spanned_messages = 0; + for message in conversation.messages(cx) { + if message.offset_range.start >= selection.range().end { + break; + } else if message.offset_range.end >= selection.range().start { + let range = cmp::max(message.offset_range.start, selection.range().start) + ..cmp::min(message.offset_range.end, selection.range().end); + if !range.is_empty() { + spanned_messages += 1; + write!(&mut copied_text, "## {}\n\n", message.role).unwrap(); + for chunk in conversation.buffer.read(cx).text_for_range(range) { + copied_text.push_str(&chunk); + } + copied_text.push('\n'); + } + } + } + + if spanned_messages > 1 { + cx.platform() + .write_to_clipboard(ClipboardItem::new(copied_text)); + return; + } + } + + cx.propagate_action(); + } + + fn split(&mut self, _: &Split, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + let selections = self.editor.read(cx).selections.disjoint_anchors(); + for selection in selections.into_iter() { + let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx); + let range = selection + .map(|endpoint| endpoint.to_offset(&buffer)) + .range(); + conversation.split_message(range, cx); + } + }); + } + + fn save(&mut self, _: &Save, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + conversation.save(None, self.fs.clone(), cx) + }); + } + + fn cycle_model(&mut self, cx: &mut ViewContext) { + self.conversation.update(cx, |conversation, cx| { + let new_model = conversation.model.cycle(); + conversation.set_model(new_model, cx); + }); + } + + fn title(&self, cx: &AppContext) -> String { + self.conversation + .read(cx) + .summary + .as_ref() + .map(|summary| summary.text.clone()) + .unwrap_or_else(|| "New Conversation".into()) + } + + fn render_current_model( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> impl Element { + enum Model {} + + MouseEventHandler::new::(0, cx, |state, cx| { + let style = style.model.style_for(state); + let model_display_name = self.conversation.read(cx).model.short_name(); + Label::new(model_display_name, style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) + } + + fn render_remaining_tokens( + &self, + style: &AssistantStyle, + cx: &mut ViewContext, + ) -> Option> { + let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; + let remaining_tokens_style = if remaining_tokens <= 0 { + &style.no_remaining_tokens + } else if remaining_tokens <= 500 { + &style.low_remaining_tokens + } else { + &style.remaining_tokens + }; + Some( + Label::new( + remaining_tokens.to_string(), + remaining_tokens_style.text.clone(), + ) + .contained() + .with_style(remaining_tokens_style.container), + ) + } +} + +impl EventEmitter for ConversationEditor {} + +impl View for ConversationEditor { + fn ui_name() -> &'static str { + "ConversationEditor" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &theme::current(cx).assistant; + Stack::new() + .with_child( + ChildView::new(&self.editor, cx) + .contained() + .with_style(theme.container), + ) + .with_child( + Flex::row() + .with_child(self.render_current_model(theme, cx)) + .with_children(self.render_remaining_tokens(theme, cx)) + .aligned() + .top() + .right(), + ) + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.editor); + } + } +} + +#[derive(Clone, Debug)] +struct MessageAnchor { + id: MessageId, + start: language::Anchor, +} + +#[derive(Clone, Debug)] +pub struct Message { + offset_range: Range, + index_range: Range, + id: MessageId, + anchor: language::Anchor, + role: Role, + sent_at: DateTime, + status: MessageStatus, +} + +impl Message { + fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage { + let content = buffer + .text_for_range(self.offset_range.clone()) + .collect::(); + RequestMessage { + role: self.role, + content: content.trim_end().into(), + } + } +} + +enum InlineAssistantEvent { + Confirmed { + prompt: String, + include_conversation: bool, + retrieve_context: bool, + }, + Canceled, + Dismissed, + IncludeConversationToggled { + include_conversation: bool, + }, + RetrieveContextToggled { + retrieve_context: bool, + }, +} + +struct InlineAssistant { + id: usize, + prompt_editor: View, + workspace: WeakView, + confirmed: bool, + has_focus: bool, + include_conversation: bool, + measurements: Rc>, + prompt_history: VecDeque, + prompt_history_ix: Option, + pending_prompt: String, + codegen: Model, + _subscriptions: Vec, + retrieve_context: bool, + semantic_index: Option>, + semantic_permissioned: Option, + project: WeakModel, + maintain_rate_limit: Option>, +} + +impl Entity for InlineAssistant { + type Event = InlineAssistantEvent; +} + +impl View for InlineAssistant { + fn ui_name() -> &'static str { + "InlineAssistant" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + enum ErrorIcon {} + let theme = theme::current(cx); + + Flex::row() + .with_children([Flex::row() + .with_child( + Button::action(ToggleIncludeConversation) + .with_tooltip("Include Conversation", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) + .toggleable(self.include_conversation) + .with_style(theme.assistant.inline.include_conversation.clone()) + .element() + .aligned(), + ) + .with_children(if SemanticIndex::enabled(cx) { + Some( + Button::action(ToggleRetrieveContext) + .with_tooltip("Retrieve Context", theme.tooltip.clone()) + .with_id(self.id) + .with_contents(theme::components::svg::Svg::new( + "icons/magnifying_glass.svg", + )) + .toggleable(self.retrieve_context) + .with_style(theme.assistant.inline.retrieve_context.clone()) + .element() + .aligned(), + ) + } else { + None + }) + .with_children(if let Some(error) = self.codegen.read(cx).error() { + Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + error.to_string(), + None, + theme.tooltip.clone(), + cx, + ) + .aligned(), + ) + } else { + None + }) + .aligned() + .constrained() + .dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f(measurements.gutter_width, constraint.min.y()), + max: vec2f(measurements.gutter_width, constraint.max.y()), + } + } + })]) + .with_child(Empty::new().constrained().dynamically({ + let measurements = self.measurements.clone(); + move |constraint, _, _| { + let measurements = measurements.get(); + SizeConstraint { + min: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.min.y(), + ), + max: vec2f( + measurements.anchor_x - measurements.gutter_width, + constraint.max.y(), + ), + } + } + })) + .with_child( + ChildView::new(&self.prompt_editor, cx) + .aligned() + .left() + .flex(1., true), + ) + .with_children(if self.retrieve_context { + Some( + Flex::row() + .with_children(self.retrieve_context_status(cx)) + .flex(1., true) + .aligned(), + ) + } else { + None + }) + .contained() + .with_style(theme.assistant.inline.container) + .into_any() + .into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + cx.focus(&self.prompt_editor); + self.has_focus = true; + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } +} + +impl InlineAssistant { + fn new( + id: usize, + measurements: Rc>, + include_conversation: bool, + prompt_history: VecDeque, + codegen: Model, + workspace: WeakView, + cx: &mut ViewContext, + retrieve_context: bool, + semantic_index: Option>, + project: Model, + ) -> Self { + let prompt_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + let placeholder = match codegen.read(cx).kind() { + CodegenKind::Transform { .. } => "Enter transformation prompt…", + CodegenKind::Generate { .. } => "Enter generation prompt…", + }; + editor.set_placeholder_text(placeholder, cx); + editor + }); + let mut subscriptions = vec![ + cx.observe(&codegen, Self::handle_codegen_changed), + cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), + ]; + + if let Some(semantic_index) = semantic_index.clone() { + subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed)); + } + + let assistant = Self { + id, + prompt_editor, + workspace, + confirmed: false, + has_focus: false, + include_conversation, + measurements, + prompt_history, + prompt_history_ix: None, + pending_prompt: String::new(), + codegen, + _subscriptions: subscriptions, + retrieve_context, + semantic_permissioned: None, + semantic_index, + project: project.downgrade(), + maintain_rate_limit: None, + }; + + assistant.index_project(cx).log_err(); + + assistant + } + + fn semantic_permissioned(&self, cx: &mut ViewContext) -> Task> { + if let Some(value) = self.semantic_permissioned { + return Task::ready(Ok(value)); + } + + let Some(project) = self.project.upgrade() else { + return Task::ready(Err(anyhow!("project was dropped"))); + }; + + self.semantic_index + .as_ref() + .map(|semantic| { + semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx)) + }) + .unwrap_or(Task::ready(Ok(false))) + } + + fn handle_prompt_editor_events( + &mut self, + _: View, + event: &EditorEvent, + cx: &mut ViewContext, + ) { + if let EditorEvent::Edited = event { + self.pending_prompt = self.prompt_editor.read(cx).text(cx); + cx.notify(); + } + } + + fn semantic_index_changed( + &mut self, + semantic_index: Model, + cx: &mut ViewContext, + ) { + let Some(project) = self.project.upgrade() else { + return; + }; + + let status = semantic_index.read(cx).status(&project); + match status { + SemanticIndexStatus::Indexing { + rate_limit_expiry: Some(_), + .. + } => { + if self.maintain_rate_limit.is_none() { + self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move { + loop { + cx.background_executor().timer(Duration::from_secs(1)).await; + this.update(&mut cx, |_, cx| cx.notify()).log_err(); + } + })); + } + return; + } + _ => { + self.maintain_rate_limit = None; + } + } + } + + fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { + let is_read_only = !self.codegen.read(cx).idle(); + self.prompt_editor.update(cx, |editor, cx| { + let was_read_only = editor.read_only(); + if was_read_only != is_read_only { + if is_read_only { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + } else { + self.confirmed = false; + editor.set_read_only(false); + editor.set_field_editor_style( + Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), + cx, + ); + } + } + }); + cx.notify(); + } + + fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext) { + cx.emit(InlineAssistantEvent::Canceled); + } + + fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { + if self.confirmed { + cx.emit(InlineAssistantEvent::Dismissed); + } else { + report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); + + let prompt = self.prompt_editor.read(cx).text(cx); + self.prompt_editor.update(cx, |editor, cx| { + editor.set_read_only(true); + editor.set_field_editor_style( + Some(Arc::new(|theme| { + theme.assistant.inline.disabled_editor.clone() + })), + cx, + ); + }); + cx.emit(InlineAssistantEvent::Confirmed { + prompt, + include_conversation: self.include_conversation, + retrieve_context: self.retrieve_context, + }); + self.confirmed = true; + cx.notify(); + } + } + + fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext) { + let semantic_permissioned = self.semantic_permissioned(cx); + + let Some(project) = self.project.upgrade() else { + return; + }; + + let project_name = project + .read(cx) + .worktree_root_names(cx) + .collect::>() + .join("/"); + let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0; + let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name, + if is_plural { + "s" + } else {""}); + + cx.spawn(|this, mut cx| async move { + // If Necessary prompt user + if !semantic_permissioned.await.unwrap_or(false) { + let mut answer = this.update(&mut cx, |_, cx| { + cx.prompt( + PromptLevel::Info, + prompt_text.as_str(), + &["Continue", "Cancel"], + ) + })?; + + if answer.next().await == Some(0) { + this.update(&mut cx, |this, _| { + this.semantic_permissioned = Some(true); + })?; + } else { + return anyhow::Ok(()); + } + } + + // If permissioned, update context appropriately + this.update(&mut cx, |this, cx| { + this.retrieve_context = !this.retrieve_context; + + cx.emit(InlineAssistantEvent::RetrieveContextToggled { + retrieve_context: this.retrieve_context, + }); + + if this.retrieve_context { + this.index_project(cx).log_err(); + } + + cx.notify(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn index_project(&self, cx: &mut ViewContext) -> anyhow::Result<()> { + let Some(project) = self.project.upgrade() else { + return Err(anyhow!("project was dropped!")); + }; + + let semantic_permissioned = self.semantic_permissioned(cx); + if let Some(semantic_index) = SemanticIndex::global(cx) { + cx.spawn(|_, mut cx| async move { + // This has to be updated to accomodate for semantic_permissions + if semantic_permissioned.await.unwrap_or(false) { + semantic_index + .update(&mut cx, |index, cx| index.index_project(project, cx)) + .await + } else { + Err(anyhow!("project is not permissioned for semantic indexing")) + } + }) + .detach_and_log_err(cx); + } + + anyhow::Ok(()) + } + + fn retrieve_context_status( + &self, + cx: &mut ViewContext, + ) -> Option> { + enum ContextStatusIcon {} + + let Some(project) = self.project.upgrade() else { + return None; + }; + + if let Some(semantic_index) = SemanticIndex::global(cx) { + let status = semantic_index.update(cx, |index, _| index.status(&project)); + let theme = theme::current(cx); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.error_icon.color) + .constrained() + .with_width(theme.assistant.error_icon.width) + .contained() + .with_style(theme.assistant.error_icon.container) + .with_tooltip::( + self.id, + "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::NotIndexed {} => Some( + Svg::new("icons/error.svg") + .with_color(theme.assistant.inline.context_status.error_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.error_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.error_icon.container) + .with_tooltip::( + self.id, + "Not Indexed", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); + } + } + Some( + Svg::new("icons/update.svg") + .with_color(theme.assistant.inline.context_status.in_progress_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.in_progress_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.in_progress_icon.container) + .with_tooltip::( + self.id, + status_text, + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ) + } + SemanticIndexStatus::Indexed {} => Some( + Svg::new("icons/check.svg") + .with_color(theme.assistant.inline.context_status.complete_icon.color) + .constrained() + .with_width(theme.assistant.inline.context_status.complete_icon.width) + .contained() + .with_style(theme.assistant.inline.context_status.complete_icon.container) + .with_tooltip::( + self.id, + "Index up to date", + None, + theme.tooltip.clone(), + cx, + ) + .aligned() + .into_any(), + ), + } + } else { + None + } + } + + // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { + // let project = self.project.clone(); + // if let Some(semantic_index) = self.semantic_index.clone() { + // let status = semantic_index.update(cx, |index, cx| index.status(&project)); + // return match status { + // // This theoretically shouldnt be a valid code path + // // As the inline assistant cant be launched without an API key + // // We keep it here for safety + // semantic_index::SemanticIndexStatus::NotAuthenticated => { + // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexed => { + // "Indexing Complete!".to_string() + // } + // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { + + // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); + + // if let Some(rate_limit_expiry) = rate_limit_expiry { + // let remaining_seconds = + // rate_limit_expiry.duration_since(Instant::now()); + // if remaining_seconds > Duration::from_secs(0) { + // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); + // } + // } + // status + // } + // semantic_index::SemanticIndexStatus::NotIndexed => { + // "Not Indexed for Context Retrieval".to_string() + // } + // }; + // } + + // "".to_string() + // } + + fn toggle_include_conversation( + &mut self, + _: &ToggleIncludeConversation, + cx: &mut ViewContext, + ) { + self.include_conversation = !self.include_conversation; + cx.emit(InlineAssistantEvent::IncludeConversationToggled { + include_conversation: self.include_conversation, + }); + cx.notify(); + } + + fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix > 0 { + self.prompt_history_ix = Some(ix - 1); + let prompt = self.prompt_history[ix - 1].clone(); + self.set_prompt(&prompt, cx); + } + } else if !self.prompt_history.is_empty() { + self.prompt_history_ix = Some(self.prompt_history.len() - 1); + let prompt = self.prompt_history[self.prompt_history.len() - 1].clone(); + self.set_prompt(&prompt, cx); + } + } + + fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext) { + if let Some(ix) = self.prompt_history_ix { + if ix < self.prompt_history.len() - 1 { + self.prompt_history_ix = Some(ix + 1); + let prompt = self.prompt_history[ix + 1].clone(); + self.set_prompt(&prompt, cx); + } else { + self.prompt_history_ix = None; + let pending_prompt = self.pending_prompt.clone(); + self.set_prompt(&pending_prompt, cx); + } + } + } + + fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext) { + self.prompt_editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + let len = buffer.len(cx); + buffer.edit([(0..len, prompt)], None, cx); + }); + }); + } +} + +// This wouldn't need to exist if we could pass parameters when rendering child views. +#[derive(Copy, Clone, Default)] +struct BlockMeasurements { + anchor_x: f32, + gutter_width: f32, +} + +struct PendingInlineAssist { + editor: WeakView, + inline_assistant: Option<(BlockId, View)>, + codegen: Model, + _subscriptions: Vec, + project: WeakModel, +} + +fn merge_ranges(ranges: &mut Vec>, buffer: &MultiBufferSnapshot) { + ranges.sort_unstable_by(|a, b| { + a.start + .cmp(&b.start, buffer) + .then_with(|| b.end.cmp(&a.end, buffer)) + }); + + let mut ix = 0; + while ix + 1 < ranges.len() { + let b = ranges[ix + 1].clone(); + let a = &mut ranges[ix]; + if a.end.cmp(&b.start, buffer).is_gt() { + if a.end.cmp(&b.end, buffer).is_lt() { + a.end = b.end; + } + ranges.remove(ix + 1); + } else { + ix += 1; + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::MessageId; + use ai::test::FakeCompletionProvider; + use gpui::AppContext; + + #[gpui::test] + fn test_inserting_and_removing_messages(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + + let completion_provider = Arc::new(FakeCompletionProvider::new()); + let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + let message_2 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..1), + (message_2.id, Role::Assistant, 1..1) + ] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "1"), (1..1, "2")], None, cx) + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..3) + ] + ); + + let message_3 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_3.id, Role::User, 4..4) + ] + ); + + let message_4 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_4.id, Role::User, 4..5), + (message_3.id, Role::User, 5..5), + ] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(4..4, "C"), (5..5, "D")], None, cx) + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_4.id, Role::User, 4..6), + (message_3.id, Role::User, 6..7), + ] + ); + + // Deleting across message boundaries merges the messages. + buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx)); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..3), + (message_3.id, Role::User, 3..4), + ] + ); + + // Undoing the deletion should also undo the merge. + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..2), + (message_2.id, Role::Assistant, 2..4), + (message_4.id, Role::User, 4..6), + (message_3.id, Role::User, 6..7), + ] + ); + + // Redoing the deletion should also redo the merge. + buffer.update(cx, |buffer, cx| buffer.redo(cx)); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..3), + (message_3.id, Role::User, 3..4), + ] + ); + + // Ensure we can still insert after a merged message. + let message_5 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..3), + (message_5.id, Role::System, 3..4), + (message_3.id, Role::User, 4..5) + ] + ); + } + + #[gpui::test] + fn test_message_splitting(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); + + let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx) + }); + + let (_, message_2) = + conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); + let message_2 = message_2.unwrap(); + + // We recycle newlines in the middle of a split message + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..16), + ] + ); + + let (_, message_3) = + conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx)); + let message_3 = message_3.unwrap(); + + // We don't recycle newlines at the end of a split message + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..17), + ] + ); + + let (_, message_4) = + conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); + let message_4 = message_4.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..17), + ] + ); + + let (_, message_5) = + conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx)); + let message_5 = message_5.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..10), + (message_5.id, Role::User, 10..18), + ] + ); + + let (message_6, message_7) = conversation.update(cx, |conversation, cx| { + conversation.split_message(14..16, cx) + }); + let message_6 = message_6.unwrap(); + let message_7 = message_7.unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_3.id, Role::User, 4..5), + (message_2.id, Role::User, 5..9), + (message_4.id, Role::User, 9..10), + (message_5.id, Role::User, 10..14), + (message_6.id, Role::User, 14..17), + (message_7.id, Role::User, 17..19), + ] + ); + } + + #[gpui::test] + fn test_messages_for_offsets(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); + let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + + let message_1 = conversation.read(cx).message_anchors[0].clone(); + assert_eq!( + messages(&conversation, cx), + vec![(message_1.id, Role::User, 0..0)] + ); + + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx)); + let message_2 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx)); + + let message_3 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx)); + + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..8), + (message_3.id, Role::User, 8..11) + ] + ); + + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 4, 9], cx), + [message_1.id, message_2.id, message_3.id] + ); + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 1, 11], cx), + [message_1.id, message_3.id] + ); + + let message_4 = conversation + .update(cx, |conversation, cx| { + conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx) + }) + .unwrap(); + assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n"); + assert_eq!( + messages(&conversation, cx), + vec![ + (message_1.id, Role::User, 0..4), + (message_2.id, Role::User, 4..8), + (message_3.id, Role::User, 8..12), + (message_4.id, Role::User, 12..12) + ] + ); + assert_eq!( + message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx), + [message_1.id, message_2.id, message_3.id, message_4.id] + ); + + fn message_ids_for_offsets( + conversation: &Model, + offsets: &[usize], + cx: &AppContext, + ) -> Vec { + conversation + .read(cx) + .messages_for_offsets(offsets.iter().copied(), cx) + .into_iter() + .map(|message| message.id) + .collect() + } + } + + #[gpui::test] + fn test_serialization(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + init(cx); + let registry = Arc::new(LanguageRegistry::test()); + let completion_provider = Arc::new(FakeCompletionProvider::new()); + let conversation = + cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); + let buffer = conversation.read(cx).buffer.clone(); + let message_0 = conversation.read(cx).message_anchors[0].id; + let message_1 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx) + .unwrap() + }); + let message_2 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx); + buffer.finalize_last_transaction(); + }); + let _message_3 = conversation.update(cx, |conversation, cx| { + conversation + .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx) + .unwrap() + }); + buffer.update(cx, |buffer, cx| buffer.undo(cx)); + assert_eq!(buffer.read(cx).text(), "a\nb\nc\n"); + assert_eq!( + messages(&conversation, cx), + [ + (message_0, Role::User, 0..2), + (message_1.id, Role::Assistant, 2..6), + (message_2.id, Role::System, 6..6), + ] + ); + + let deserialized_conversation = cx.add_model(|cx| { + Conversation::deserialize( + conversation.read(cx).serialize(cx), + Default::default(), + registry.clone(), + cx, + ) + }); + let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone(); + assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n"); + assert_eq!( + messages(&deserialized_conversation, cx), + [ + (message_0, Role::User, 0..2), + (message_1.id, Role::Assistant, 2..6), + (message_2.id, Role::System, 6..6), + ] + ); + } + + fn messages( + conversation: &Model, + cx: &AppContext, + ) -> Vec<(MessageId, Role, Range)> { + conversation + .read(cx) + .messages(cx) + .map(|message| (message.id, message.role, message.offset_range)) + .collect() + } +} + +fn report_assistant_event( + workspace: WeakView, + conversation_id: Option, + assistant_kind: AssistantKind, + cx: &AppContext, +) { + let Some(workspace) = workspace.upgrade() else { + return; + }; + + let client = workspace.read(cx).project().read(cx).client(); + let telemetry = client.telemetry(); + + let model = AssistantSettings::get_global(cx) + .default_open_ai_model + .clone(); + + let telemetry_settings = TelemetrySettings::get_global(cx); + + telemetry.report_assistant_event( + telemetry_settings, + conversation_id, + assistant_kind, + model.full_name(), + ) +} diff --git a/crates/assistant2/src/assistant_settings.rs b/crates/assistant2/src/assistant_settings.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a727df621bd911ee56b4ba2b3d8b13567b35b63 --- /dev/null +++ b/crates/assistant2/src/assistant_settings.rs @@ -0,0 +1,80 @@ +use anyhow; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub enum OpenAIModel { + #[serde(rename = "gpt-3.5-turbo-0613")] + ThreePointFiveTurbo, + #[serde(rename = "gpt-4-0613")] + Four, + #[serde(rename = "gpt-4-1106-preview")] + FourTurbo, +} + +impl OpenAIModel { + pub fn full_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo-0613", + OpenAIModel::Four => "gpt-4-0613", + OpenAIModel::FourTurbo => "gpt-4-1106-preview", + } + } + + pub fn short_name(&self) -> &'static str { + match self { + OpenAIModel::ThreePointFiveTurbo => "gpt-3.5-turbo", + OpenAIModel::Four => "gpt-4", + OpenAIModel::FourTurbo => "gpt-4-turbo", + } + } + + pub fn cycle(&self) -> Self { + match self { + OpenAIModel::ThreePointFiveTurbo => OpenAIModel::Four, + OpenAIModel::Four => OpenAIModel::FourTurbo, + OpenAIModel::FourTurbo => OpenAIModel::ThreePointFiveTurbo, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum AssistantDockPosition { + Left, + Right, + Bottom, +} + +#[derive(Deserialize, Debug)] +pub struct AssistantSettings { + pub button: bool, + pub dock: AssistantDockPosition, + pub default_width: f32, + pub default_height: f32, + pub default_open_ai_model: OpenAIModel, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct AssistantSettingsContent { + pub button: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, + pub default_open_ai_model: Option, +} + +impl Settings for AssistantSettings { + const KEY: Option<&'static str> = Some("assistant"); + + type FileContent = AssistantSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs new file mode 100644 index 0000000000000000000000000000000000000000..9696c629ac910010d193057adcdb1db3f2070935 --- /dev/null +++ b/crates/assistant2/src/codegen.rs @@ -0,0 +1,695 @@ +use crate::streaming_diff::{Hunk, StreamingDiff}; +use ai::completion::{CompletionProvider, CompletionRequest}; +use anyhow::Result; +use editor::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; +use futures::{channel::mpsc, SinkExt, Stream, StreamExt}; +use gpui::{EventEmitter, Model, ModelContext, Task}; +use language::{Rope, TransactionId}; +use multi_buffer; +use std::{cmp, future, ops::Range, sync::Arc}; + +pub enum Event { + Finished, + Undone, +} + +#[derive(Clone)] +pub enum CodegenKind { + Transform { range: Range }, + Generate { position: Anchor }, +} + +pub struct Codegen { + provider: Arc, + buffer: Model, + snapshot: MultiBufferSnapshot, + kind: CodegenKind, + last_equal_ranges: Vec>, + transaction_id: Option, + error: Option, + generation: Task<()>, + idle: bool, + _subscription: gpui::Subscription, +} + +impl EventEmitter for Codegen {} + +impl Codegen { + pub fn new( + buffer: Model, + kind: CodegenKind, + provider: Arc, + cx: &mut ModelContext, + ) -> Self { + let snapshot = buffer.read(cx).snapshot(cx); + Self { + provider, + buffer: buffer.clone(), + snapshot, + kind, + last_equal_ranges: Default::default(), + transaction_id: Default::default(), + error: Default::default(), + idle: true, + generation: Task::ready(()), + _subscription: cx.subscribe(&buffer, Self::handle_buffer_event), + } + } + + fn handle_buffer_event( + &mut self, + _buffer: Model, + event: &multi_buffer::Event, + cx: &mut ModelContext, + ) { + if let multi_buffer::Event::TransactionUndone { transaction_id } = event { + if self.transaction_id == Some(*transaction_id) { + self.transaction_id = None; + self.generation = Task::ready(()); + cx.emit(Event::Undone); + } + } + } + + pub fn range(&self) -> Range { + match &self.kind { + CodegenKind::Transform { range } => range.clone(), + CodegenKind::Generate { position } => position.bias_left(&self.snapshot)..*position, + } + } + + pub fn kind(&self) -> &CodegenKind { + &self.kind + } + + pub fn last_equal_ranges(&self) -> &[Range] { + &self.last_equal_ranges + } + + pub fn idle(&self) -> bool { + self.idle + } + + pub fn error(&self) -> Option<&anyhow::Error> { + self.error.as_ref() + } + + pub fn start(&mut self, prompt: Box, cx: &mut ModelContext) { + let range = self.range(); + let snapshot = self.snapshot.clone(); + let selected_text = snapshot + .text_for_range(range.start..range.end) + .collect::(); + + let selection_start = range.start.to_point(&snapshot); + let suggested_line_indent = snapshot + .suggested_indents(selection_start.row..selection_start.row + 1, cx) + .into_values() + .next() + .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); + + let response = self.provider.complete(prompt); + self.generation = cx.spawn_weak(|this, mut cx| { + async move { + let generate = async { + let mut edit_start = range.start.to_offset(&snapshot); + + let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); + let diff = cx.background().spawn(async move { + let chunks = strip_invalid_spans_from_codeblock(response.await?); + futures::pin_mut!(chunks); + let mut diff = StreamingDiff::new(selected_text.to_string()); + + let mut new_text = String::new(); + let mut base_indent = None; + let mut line_indent = None; + let mut first_line = true; + + while let Some(chunk) = chunks.next().await { + let chunk = chunk?; + + let mut lines = chunk.split('\n').peekable(); + while let Some(line) = lines.next() { + new_text.push_str(line); + if line_indent.is_none() { + if let Some(non_whitespace_ch_ix) = + new_text.find(|ch: char| !ch.is_whitespace()) + { + line_indent = Some(non_whitespace_ch_ix); + base_indent = base_indent.or(line_indent); + + let line_indent = line_indent.unwrap(); + let base_indent = base_indent.unwrap(); + let indent_delta = line_indent as i32 - base_indent as i32; + let mut corrected_indent_len = cmp::max( + 0, + suggested_line_indent.len as i32 + indent_delta, + ) + as usize; + if first_line { + corrected_indent_len = corrected_indent_len + .saturating_sub(selection_start.column as usize); + } + + let indent_char = suggested_line_indent.char(); + let mut indent_buffer = [0; 4]; + let indent_str = + indent_char.encode_utf8(&mut indent_buffer); + new_text.replace_range( + ..line_indent, + &indent_str.repeat(corrected_indent_len), + ); + } + } + + if line_indent.is_some() { + hunks_tx.send(diff.push_new(&new_text)).await?; + new_text.clear(); + } + + if lines.peek().is_some() { + hunks_tx.send(diff.push_new("\n")).await?; + line_indent = None; + first_line = false; + } + } + } + hunks_tx.send(diff.push_new(&new_text)).await?; + hunks_tx.send(diff.finish()).await?; + + anyhow::Ok(()) + }); + + while let Some(hunks) = hunks_rx.next().await { + let this = if let Some(this) = this.upgrade(&cx) { + this + } else { + break; + }; + + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + + let transaction = this.buffer.update(cx, |buffer, cx| { + // Avoid grouping assistant edits with user edits. + buffer.finalize_last_transaction(cx); + + buffer.start_transaction(cx); + buffer.edit( + hunks.into_iter().filter_map(|hunk| match hunk { + Hunk::Insert { text } => { + let edit_start = snapshot.anchor_after(edit_start); + Some((edit_start..edit_start, text)) + } + Hunk::Remove { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + Some((edit_range, String::new())) + } + Hunk::Keep { len } => { + let edit_end = edit_start + len; + let edit_range = snapshot.anchor_after(edit_start) + ..snapshot.anchor_before(edit_end); + edit_start = edit_end; + this.last_equal_ranges.push(edit_range); + None + } + }), + None, + cx, + ); + + buffer.end_transaction(cx) + }); + + if let Some(transaction) = transaction { + if let Some(first_transaction) = this.transaction_id { + // Group all assistant edits into the first transaction. + this.buffer.update(cx, |buffer, cx| { + buffer.merge_transactions( + transaction, + first_transaction, + cx, + ) + }); + } else { + this.transaction_id = Some(transaction); + this.buffer.update(cx, |buffer, cx| { + buffer.finalize_last_transaction(cx) + }); + } + } + + cx.notify(); + }); + } + + diff.await?; + anyhow::Ok(()) + }; + + let result = generate.await; + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }); + } + } + }); + self.error.take(); + self.idle = false; + cx.notify(); + } + + pub fn undo(&mut self, cx: &mut ModelContext) { + if let Some(transaction_id) = self.transaction_id { + self.buffer + .update(cx, |buffer, cx| buffer.undo_transaction(transaction_id, cx)); + } + } +} + +fn strip_invalid_spans_from_codeblock( + stream: impl Stream>, +) -> impl Stream> { + let mut first_line = true; + let mut buffer = String::new(); + let mut starts_with_markdown_codeblock = false; + let mut includes_start_or_end_span = false; + stream.filter_map(move |chunk| { + let chunk = match chunk { + Ok(chunk) => chunk, + Err(err) => return future::ready(Some(Err(err))), + }; + buffer.push_str(&chunk); + + if buffer.len() > "<|S|".len() && buffer.starts_with("<|S|") { + includes_start_or_end_span = true; + + buffer = buffer + .strip_prefix("<|S|>") + .or_else(|| buffer.strip_prefix("<|S|")) + .unwrap_or(&buffer) + .to_string(); + } else if buffer.ends_with("|E|>") { + includes_start_or_end_span = true; + } else if buffer.starts_with("<|") + || buffer.starts_with("<|S") + || buffer.starts_with("<|S|") + || buffer.ends_with("|") + || buffer.ends_with("|E") + || buffer.ends_with("|E|") + { + return future::ready(None); + } + + if first_line { + if buffer == "" || buffer == "`" || buffer == "``" { + return future::ready(None); + } else if buffer.starts_with("```") { + starts_with_markdown_codeblock = true; + if let Some(newline_ix) = buffer.find('\n') { + buffer.replace_range(..newline_ix + 1, ""); + first_line = false; + } else { + return future::ready(None); + } + } + } + + let mut text = buffer.to_string(); + if starts_with_markdown_codeblock { + text = text + .strip_suffix("\n```\n") + .or_else(|| text.strip_suffix("\n```")) + .or_else(|| text.strip_suffix("\n``")) + .or_else(|| text.strip_suffix("\n`")) + .or_else(|| text.strip_suffix('\n')) + .unwrap_or(&text) + .to_string(); + } + + if includes_start_or_end_span { + text = text + .strip_suffix("|E|>") + .or_else(|| text.strip_suffix("E|>")) + .or_else(|| text.strip_prefix("|>")) + .or_else(|| text.strip_prefix(">")) + .unwrap_or(&text) + .to_string(); + }; + + if text.contains('\n') { + first_line = false; + } + + let remainder = buffer.split_off(text.len()); + let result = if buffer.is_empty() { + None + } else { + Some(Ok(buffer.clone())) + }; + + buffer = remainder; + future::ready(result) + }) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use super::*; + use ai::test::FakeCompletionProvider; + use futures::stream::{self}; + use gpui::TestAppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use rand::prelude::*; + use serde::Serialize; + use settings::SettingsStore; + + #[derive(Serialize)] + pub struct DummyCompletionRequest { + pub name: String, + } + + impl CompletionRequest for DummyCompletionRequest { + fn data(&self) -> serde_json::Result { + serde_json::to_string(self) + } + } + + #[gpui::test(iterations = 10)] + async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + let x = 0; + for _ in 0..10 { + x += 1; + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let range = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) + }); + let provider = Arc::new(FakeCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Transform { range }, + provider.clone(), + cx, + ) + }); + + let request = Box::new(DummyCompletionRequest { + name: "test".to_string(), + }); + codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + + let mut new_text = concat!( + " let mut x = 0;\n", + " while x < 10 {\n", + " x += 1;\n", + " }", + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + println!("CHUNK: {:?}", &chunk); + provider.send_completion(chunk); + new_text = suffix; + cx.background_executor.run_until_parked(); + } + provider.finish_completion(); + cx.background_executor.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_past_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = indoc! {" + fn main() { + le + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 6)) + }); + let provider = Arc::new(FakeCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + + let request = Box::new(DummyCompletionRequest { + name: "test".to_string(), + }); + codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + + let mut new_text = concat!( + "t mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + provider.send_completion(chunk); + new_text = suffix; + cx.background_executor.run_until_parked(); + } + provider.finish_completion(); + cx.background_executor.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test(iterations = 10)] + async fn test_autoindent_when_generating_before_indentation( + cx: &mut TestAppContext, + mut rng: StdRng, + ) { + cx.set_global(cx.read(SettingsStore::test)); + cx.update(language_settings::init); + + let text = concat!( + "fn main() {\n", + " \n", + "}\n" // + ); + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + let position = buffer.read_with(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + snapshot.anchor_before(Point::new(1, 2)) + }); + let provider = Arc::new(FakeCompletionProvider::new()); + let codegen = cx.add_model(|cx| { + Codegen::new( + buffer.clone(), + CodegenKind::Generate { position }, + provider.clone(), + cx, + ) + }); + + let request = Box::new(DummyCompletionRequest { + name: "test".to_string(), + }); + codegen.update(cx, |codegen, cx| codegen.start(request, cx)); + + let mut new_text = concat!( + "let mut x = 0;\n", + "while x < 10 {\n", + " x += 1;\n", + "}", // + ); + while !new_text.is_empty() { + let max_len = cmp::min(new_text.len(), 10); + let len = rng.gen_range(1..=max_len); + let (chunk, suffix) = new_text.split_at(len); + println!("{:?}", &chunk); + provider.send_completion(chunk); + new_text = suffix; + cx.background_executor.run_until_parked(); + } + provider.finish_completion(); + cx.background_executor.run_until_parked(); + + assert_eq!( + buffer.read_with(cx, |buffer, cx| buffer.snapshot(cx).text()), + indoc! {" + fn main() { + let mut x = 0; + while x < 10 { + x += 1; + } + } + "} + ); + } + + #[gpui::test] + async fn test_strip_invalid_spans_from_codeblock() { + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("Lorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\nLorem ipsum dolor\n```\n", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum dolor" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks( + "```html\n```js\nLorem ipsum dolor\n```\n```", + 2 + )) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "```js\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("``\nLorem ipsum dolor\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "``\nLorem ipsum dolor\n```" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("<|S|Lorem ipsum|E|>", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("<|S|>Lorem ipsum", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\n<|S|>Lorem ipsum\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + assert_eq!( + strip_invalid_spans_from_codeblock(chunks("```\n<|S|Lorem ipsum|E|>\n```", 2)) + .map(|chunk| chunk.unwrap()) + .collect::() + .await, + "Lorem ipsum" + ); + fn chunks(text: &str, size: usize) -> impl Stream> { + stream::iter( + text.chars() + .collect::>() + .chunks(size) + .map(|chunk| Ok(chunk.iter().collect::())) + .collect::>(), + ) + } + } + + fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_indents_query( + r#" + (call_expression) @indent + (field_expression) @indent + (_ "(" ")" @end) @indent + (_ "{" "}" @end) @indent + "#, + ) + .unwrap() + } +} diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs new file mode 100644 index 0000000000000000000000000000000000000000..b678c6fe3b402129692b02f0094841cafe92ffca --- /dev/null +++ b/crates/assistant2/src/prompts.rs @@ -0,0 +1,388 @@ +use ai::models::LanguageModel; +use ai::prompts::base::{PromptArguments, PromptChain, PromptPriority, PromptTemplate}; +use ai::prompts::file_context::FileContext; +use ai::prompts::generate::GenerateInlineContent; +use ai::prompts::preamble::EngineerPreamble; +use ai::prompts::repository_context::{PromptCodeSnippet, RepositoryContext}; +use ai::providers::open_ai::OpenAILanguageModel; +use language::{BufferSnapshot, OffsetRangeExt, ToOffset}; +use std::cmp::{self, Reverse}; +use std::ops::Range; +use std::sync::Arc; + +#[allow(dead_code)] +fn summarize(buffer: &BufferSnapshot, selected_range: Range) -> String { + #[derive(Debug)] + struct Match { + collapse: Range, + keep: Vec>, + } + + let selected_range = selected_range.to_offset(buffer); + let mut ts_matches = buffer.matches(0..buffer.len(), |grammar| { + Some(&grammar.embedding_config.as_ref()?.query) + }); + let configs = ts_matches + .grammars() + .iter() + .map(|g| g.embedding_config.as_ref().unwrap()) + .collect::>(); + let mut matches = Vec::new(); + while let Some(mat) = ts_matches.peek() { + let config = &configs[mat.grammar_index]; + if let Some(collapse) = mat.captures.iter().find_map(|cap| { + if Some(cap.index) == config.collapse_capture_ix { + Some(cap.node.byte_range()) + } else { + None + } + }) { + let mut keep = Vec::new(); + for capture in mat.captures.iter() { + if Some(capture.index) == config.keep_capture_ix { + keep.push(capture.node.byte_range()); + } else { + continue; + } + } + ts_matches.advance(); + matches.push(Match { collapse, keep }); + } else { + ts_matches.advance(); + } + } + matches.sort_unstable_by_key(|mat| (mat.collapse.start, Reverse(mat.collapse.end))); + let mut matches = matches.into_iter().peekable(); + + let mut summary = String::new(); + let mut offset = 0; + let mut flushed_selection = false; + while let Some(mat) = matches.next() { + // Keep extending the collapsed range if the next match surrounds + // the current one. + while let Some(next_mat) = matches.peek() { + if mat.collapse.start <= next_mat.collapse.start + && mat.collapse.end >= next_mat.collapse.end + { + matches.next().unwrap(); + } else { + break; + } + } + + if offset > mat.collapse.start { + // Skip collapsed nodes that have already been summarized. + offset = cmp::max(offset, mat.collapse.end); + continue; + } + + if offset <= selected_range.start && selected_range.start <= mat.collapse.end { + if !flushed_selection { + // The collapsed node ends after the selection starts, so we'll flush the selection first. + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|S|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|E|>"); + } + offset = selected_range.end; + flushed_selection = true; + } + + // If the selection intersects the collapsed node, we won't collapse it. + if selected_range.end >= mat.collapse.start { + continue; + } + } + + summary.extend(buffer.text_for_range(offset..mat.collapse.start)); + for keep in mat.keep { + summary.extend(buffer.text_for_range(keep)); + } + offset = mat.collapse.end; + } + + // Flush selection if we haven't already done so. + if !flushed_selection && offset <= selected_range.start { + summary.extend(buffer.text_for_range(offset..selected_range.start)); + summary.push_str("<|S|"); + if selected_range.end == selected_range.start { + summary.push_str(">"); + } else { + summary.extend(buffer.text_for_range(selected_range.clone())); + summary.push_str("|E|>"); + } + offset = selected_range.end; + } + + summary.extend(buffer.text_for_range(offset..buffer.len())); + summary +} + +pub fn generate_content_prompt( + user_prompt: String, + language_name: Option<&str>, + buffer: BufferSnapshot, + range: Range, + search_results: Vec, + model: &str, + project_name: Option, +) -> anyhow::Result { + // Using new Prompt Templates + let openai_model: Arc = Arc::new(OpenAILanguageModel::load(model)); + let lang_name = if let Some(language_name) = language_name { + Some(language_name.to_string()) + } else { + None + }; + + let args = PromptArguments { + model: openai_model, + language_name: lang_name.clone(), + project_name, + snippets: search_results.clone(), + reserved_tokens: 1000, + buffer: Some(buffer), + selected_range: Some(range), + user_prompt: Some(user_prompt.clone()), + }; + + let templates: Vec<(PromptPriority, Box)> = vec![ + (PromptPriority::Mandatory, Box::new(EngineerPreamble {})), + ( + PromptPriority::Ordered { order: 1 }, + Box::new(RepositoryContext {}), + ), + ( + PromptPriority::Ordered { order: 0 }, + Box::new(FileContext {}), + ), + ( + PromptPriority::Mandatory, + Box::new(GenerateInlineContent {}), + ), + ]; + let chain = PromptChain::new(args, templates); + let (prompt, _) = chain.generate(true)?; + + anyhow::Ok(prompt) +} + +#[cfg(test)] +pub(crate) mod tests { + + use super::*; + use std::sync::Arc; + + use gpui::AppContext; + use indoc::indoc; + use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; + use settings::SettingsStore; + + pub(crate) fn rust_lang() -> Language { + Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ) + .with_embedding_query( + r#" + ( + [(line_comment) (attribute_item)]* @context + . + [ + (struct_item + name: (_) @name) + + (enum_item + name: (_) @name) + + (impl_item + trait: (_)? @name + "for"? @name + type: (_) @name) + + (trait_item + name: (_) @name) + + (function_item + name: (_) @name + body: (block + "{" @keep + "}" @keep) @collapse) + + (macro_definition + name: (_) @name) + ] @item + ) + "#, + ) + .unwrap() + } + + #[gpui::test] + fn test_outline_for_prompt(cx: &mut AppContext) { + cx.set_global(SettingsStore::test(cx)); + language_settings::init(cx); + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + self.a + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + let buffer = + cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let snapshot = buffer.read(cx).snapshot(); + + assert_eq!( + summarize(&snapshot, Point::new(1, 4)..Point::new(1, 4)), + indoc! {" + struct X { + <|S|>a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(8, 12)..Point::new(8, 14)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let <|S|a |E|>= 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(6, 0)..Point::new(6, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + <|S|> + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + + assert_eq!( + summarize(&snapshot, Point::new(21, 0)..Point::new(21, 0)), + indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + <|S|>"} + ); + + // Ensure nested functions get collapsed properly. + let text = indoc! {" + struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self { + let a = 1; + let b = 2; + Self { a, b } + } + + pub fn a(&self, param: bool) -> usize { + let a = 30; + fn nested() -> usize { + 3 + } + self.a + nested() + } + + pub fn b(&self) -> usize { + self.b + } + } + "}; + buffer.update(cx, |buffer, cx| buffer.set_text(text, cx)); + let snapshot = buffer.read(cx).snapshot(); + assert_eq!( + summarize(&snapshot, Point::new(0, 0)..Point::new(0, 0)), + indoc! {" + <|S|>struct X { + a: usize, + b: usize, + } + + impl X { + + fn new() -> Self {} + + pub fn a(&self, param: bool) -> usize {} + + pub fn b(&self) -> usize {} + } + "} + ); + } +} diff --git a/crates/assistant2/src/streaming_diff.rs b/crates/assistant2/src/streaming_diff.rs new file mode 100644 index 0000000000000000000000000000000000000000..7399a7b4faf2629310bbf9e7ec573a651e52feaf --- /dev/null +++ b/crates/assistant2/src/streaming_diff.rs @@ -0,0 +1,293 @@ +use collections::HashMap; +use ordered_float::OrderedFloat; +use std::{ + cmp, + fmt::{self, Debug}, + ops::Range, +}; + +struct Matrix { + cells: Vec, + rows: usize, + cols: usize, +} + +impl Matrix { + fn new() -> Self { + Self { + cells: Vec::new(), + rows: 0, + cols: 0, + } + } + + fn resize(&mut self, rows: usize, cols: usize) { + self.cells.resize(rows * cols, 0.); + self.rows = rows; + self.cols = cols; + } + + fn get(&self, row: usize, col: usize) -> f64 { + if row >= self.rows { + panic!("row out of bounds") + } + + if col >= self.cols { + panic!("col out of bounds") + } + self.cells[col * self.rows + row] + } + + fn set(&mut self, row: usize, col: usize, value: f64) { + if row >= self.rows { + panic!("row out of bounds") + } + + if col >= self.cols { + panic!("col out of bounds") + } + + self.cells[col * self.rows + row] = value; + } +} + +impl Debug for Matrix { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + writeln!(f)?; + for i in 0..self.rows { + for j in 0..self.cols { + write!(f, "{:5}", self.get(i, j))?; + } + writeln!(f)?; + } + Ok(()) + } +} + +#[derive(Debug)] +pub enum Hunk { + Insert { text: String }, + Remove { len: usize }, + Keep { len: usize }, +} + +pub struct StreamingDiff { + old: Vec, + new: Vec, + scores: Matrix, + old_text_ix: usize, + new_text_ix: usize, + equal_runs: HashMap<(usize, usize), u32>, +} + +impl StreamingDiff { + const INSERTION_SCORE: f64 = -1.; + const DELETION_SCORE: f64 = -20.; + const EQUALITY_BASE: f64 = 1.8; + const MAX_EQUALITY_EXPONENT: i32 = 16; + + pub fn new(old: String) -> Self { + let old = old.chars().collect::>(); + let mut scores = Matrix::new(); + scores.resize(old.len() + 1, 1); + for i in 0..=old.len() { + scores.set(i, 0, i as f64 * Self::DELETION_SCORE); + } + Self { + old, + new: Vec::new(), + scores, + old_text_ix: 0, + new_text_ix: 0, + equal_runs: Default::default(), + } + } + + pub fn push_new(&mut self, text: &str) -> Vec { + self.new.extend(text.chars()); + self.scores.resize(self.old.len() + 1, self.new.len() + 1); + + for j in self.new_text_ix + 1..=self.new.len() { + self.scores.set(0, j, j as f64 * Self::INSERTION_SCORE); + for i in 1..=self.old.len() { + let insertion_score = self.scores.get(i, j - 1) + Self::INSERTION_SCORE; + let deletion_score = self.scores.get(i - 1, j) + Self::DELETION_SCORE; + let equality_score = if self.old[i - 1] == self.new[j - 1] { + let mut equal_run = self.equal_runs.get(&(i - 1, j - 1)).copied().unwrap_or(0); + equal_run += 1; + self.equal_runs.insert((i, j), equal_run); + + let exponent = cmp::min(equal_run as i32 / 4, Self::MAX_EQUALITY_EXPONENT); + self.scores.get(i - 1, j - 1) + Self::EQUALITY_BASE.powi(exponent) + } else { + f64::NEG_INFINITY + }; + + let score = insertion_score.max(deletion_score).max(equality_score); + self.scores.set(i, j, score); + } + } + + let mut max_score = f64::NEG_INFINITY; + let mut next_old_text_ix = self.old_text_ix; + let next_new_text_ix = self.new.len(); + for i in self.old_text_ix..=self.old.len() { + let score = self.scores.get(i, next_new_text_ix); + if score > max_score { + max_score = score; + next_old_text_ix = i; + } + } + + let hunks = self.backtrack(next_old_text_ix, next_new_text_ix); + self.old_text_ix = next_old_text_ix; + self.new_text_ix = next_new_text_ix; + hunks + } + + fn backtrack(&self, old_text_ix: usize, new_text_ix: usize) -> Vec { + let mut pending_insert: Option> = None; + let mut hunks = Vec::new(); + let mut i = old_text_ix; + let mut j = new_text_ix; + while (i, j) != (self.old_text_ix, self.new_text_ix) { + let insertion_score = if j > self.new_text_ix { + Some((i, j - 1)) + } else { + None + }; + let deletion_score = if i > self.old_text_ix { + Some((i - 1, j)) + } else { + None + }; + let equality_score = if i > self.old_text_ix && j > self.new_text_ix { + if self.old[i - 1] == self.new[j - 1] { + Some((i - 1, j - 1)) + } else { + None + } + } else { + None + }; + + let (prev_i, prev_j) = [insertion_score, deletion_score, equality_score] + .iter() + .max_by_key(|cell| cell.map(|(i, j)| OrderedFloat(self.scores.get(i, j)))) + .unwrap() + .unwrap(); + + if prev_i == i && prev_j == j - 1 { + if let Some(pending_insert) = pending_insert.as_mut() { + pending_insert.start = prev_j; + } else { + pending_insert = Some(prev_j..j); + } + } else { + if let Some(range) = pending_insert.take() { + hunks.push(Hunk::Insert { + text: self.new[range].iter().collect(), + }); + } + + let char_len = self.old[i - 1].len_utf8(); + if prev_i == i - 1 && prev_j == j { + if let Some(Hunk::Remove { len }) = hunks.last_mut() { + *len += char_len; + } else { + hunks.push(Hunk::Remove { len: char_len }) + } + } else { + if let Some(Hunk::Keep { len }) = hunks.last_mut() { + *len += char_len; + } else { + hunks.push(Hunk::Keep { len: char_len }) + } + } + } + + i = prev_i; + j = prev_j; + } + + if let Some(range) = pending_insert.take() { + hunks.push(Hunk::Insert { + text: self.new[range].iter().collect(), + }); + } + + hunks.reverse(); + hunks + } + + pub fn finish(self) -> Vec { + self.backtrack(self.old.len(), self.new.len()) + } +} + +#[cfg(test)] +mod tests { + use std::env; + + use super::*; + use rand::prelude::*; + + #[gpui::test(iterations = 100)] + fn test_random_diffs(mut rng: StdRng) { + let old_text_len = env::var("OLD_TEXT_LEN") + .map(|i| i.parse().expect("invalid `OLD_TEXT_LEN` variable")) + .unwrap_or(10); + let new_text_len = env::var("NEW_TEXT_LEN") + .map(|i| i.parse().expect("invalid `NEW_TEXT_LEN` variable")) + .unwrap_or(10); + + let old = util::RandomCharIter::new(&mut rng) + .take(old_text_len) + .collect::(); + log::info!("old text: {:?}", old); + + let mut diff = StreamingDiff::new(old.clone()); + let mut hunks = Vec::new(); + let mut new_len = 0; + let mut new = String::new(); + while new_len < new_text_len { + let new_chunk_len = rng.gen_range(1..=new_text_len - new_len); + let new_chunk = util::RandomCharIter::new(&mut rng) + .take(new_len) + .collect::(); + log::info!("new chunk: {:?}", new_chunk); + new_len += new_chunk_len; + new.push_str(&new_chunk); + let new_hunks = diff.push_new(&new_chunk); + log::info!("hunks: {:?}", new_hunks); + hunks.extend(new_hunks); + } + let final_hunks = diff.finish(); + log::info!("final hunks: {:?}", final_hunks); + hunks.extend(final_hunks); + + log::info!("new text: {:?}", new); + let mut old_ix = 0; + let mut new_ix = 0; + let mut patched = String::new(); + for hunk in hunks { + match hunk { + Hunk::Keep { len } => { + assert_eq!(&old[old_ix..old_ix + len], &new[new_ix..new_ix + len]); + patched.push_str(&old[old_ix..old_ix + len]); + old_ix += len; + new_ix += len; + } + Hunk::Remove { len } => { + old_ix += len; + } + Hunk::Insert { text } => { + assert_eq!(text, &new[new_ix..new_ix + text.len()]); + patched.push_str(&text); + new_ix += text.len(); + } + } + } + assert_eq!(patched, new); + } +} diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index c55bfa8cf5a7beec1c516598f242fcfa2fefe018..89b83a7001c5a3e2c649c8e58f30a98a5bd5fafe 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -3344,10 +3344,6 @@ impl Panel for CollabPanel { Box::new(ToggleFocus) } - fn has_focus(&self, cx: &gpui::WindowContext) -> bool { - self.focus_handle.contains_focused(cx) - } - fn persistent_name() -> &'static str { "CollabPanel" } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index ce039071cf830d3282dcc860fc6ce083576aafc1..cb6515f5297a3de20ff16b80a74a50700693438c 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -55,7 +55,6 @@ pub struct ProjectPanel { clipboard_entry: Option, _dragged_entry_destination: Option>, _workspace: WeakView, - has_focus: bool, width: Option, pending_serialization: Task>, } @@ -172,7 +171,6 @@ impl ProjectPanel { let focus_handle = cx.focus_handle(); cx.on_focus(&focus_handle, Self::focus_in).detach(); - cx.on_blur(&focus_handle, Self::focus_out).detach(); cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { @@ -238,7 +236,6 @@ impl ProjectPanel { // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), _dragged_entry_destination: None, _workspace: workspace.weak_handle(), - has_focus: false, width: None, pending_serialization: Task::ready(None), }; @@ -356,16 +353,11 @@ impl ProjectPanel { } fn focus_in(&mut self, cx: &mut ViewContext) { - if !self.has_focus { - self.has_focus = true; + if !self.focus_handle.contains_focused(cx) { cx.emit(Event::Focus); } } - fn focus_out(&mut self, _: &mut ViewContext) { - self.has_focus = false; - } - fn deploy_context_menu( &mut self, position: Point, @@ -1554,10 +1546,6 @@ impl Panel for ProjectPanel { Box::new(ToggleFocus) } - fn has_focus(&self, _: &WindowContext) -> bool { - self.has_focus - } - fn persistent_name() -> &'static str { "Project Panel" } diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index b6582b07b194331f0e33f7e5b6b70557bfa50cbc..caf339a8c65b7f93460bad67c7be5f51a3eeec4f 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -415,10 +415,6 @@ impl Panel for TerminalPanel { } } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.pane.read(cx).has_focus(cx) - } - fn persistent_name() -> &'static str { "TerminalPanel" } diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 437e7c01926a460bed320f17b6e943966d714d48..abcf5c49bc929c5954c3e40557e419e4ecaca702 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -26,6 +26,7 @@ pub trait Panel: FocusableView + EventEmitter { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); fn size(&self, cx: &WindowContext) -> f32; fn set_size(&mut self, size: Option, cx: &mut ViewContext); + // todo!("We should have a icon tooltip method, rather than using persistant_name") fn icon(&self, cx: &WindowContext) -> Option; fn toggle_action(&self) -> Box; fn icon_label(&self, _: &WindowContext) -> Option { @@ -36,7 +37,6 @@ pub trait Panel: FocusableView + EventEmitter { } fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} - fn has_focus(&self, cx: &WindowContext) -> bool; } pub trait PanelHandle: Send + Sync { @@ -53,7 +53,6 @@ pub trait PanelHandle: Send + Sync { fn icon(&self, cx: &WindowContext) -> Option; fn toggle_action(&self, cx: &WindowContext) -> Box; fn icon_label(&self, cx: &WindowContext) -> Option; - fn has_focus(&self, cx: &WindowContext) -> bool; fn focus_handle(&self, cx: &AppContext) -> FocusHandle; fn to_any(&self) -> AnyView; } @@ -114,10 +113,6 @@ where self.read(cx).icon_label(cx) } - fn has_focus(&self, cx: &WindowContext) -> bool { - self.read(cx).has_focus(cx) - } - fn to_any(&self) -> AnyView { self.clone().into() } @@ -319,7 +314,7 @@ impl Dock { } PanelEvent::ZoomIn => { this.set_panel_zoomed(&panel.to_any(), true, cx); - if !panel.has_focus(cx) { + if !panel.focus_handle(cx).contains_focused(cx) { cx.focus_view(&panel); } workspace @@ -760,7 +755,7 @@ pub mod test { pub position: DockPosition, pub zoomed: bool, pub active: bool, - pub has_focus: bool, + pub focus_handle: FocusHandle, pub size: f32, } actions!(ToggleTestPanel); @@ -768,12 +763,12 @@ pub mod test { impl EventEmitter for TestPanel {} impl TestPanel { - pub fn new(position: DockPosition) -> Self { + pub fn new(position: DockPosition, cx: &mut WindowContext) -> Self { Self { position, zoomed: false, active: false, - has_focus: false, + focus_handle: cx.focus_handle(), size: 300., } } @@ -832,15 +827,11 @@ pub mod test { fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { self.active = active; } - - fn has_focus(&self, _cx: &WindowContext) -> bool { - self.has_focus - } } impl FocusableView for TestPanel { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - unimplemented!() + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5741fa4a94cf79202f7f9b4a5694c1058be59dd5..a916ec733cd864167f5b0451133d21d879c377e4 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -65,7 +65,7 @@ use std::{ time::Duration, }; use theme::{ActiveTheme, ThemeSettings}; -pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; +pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use util::ResultExt; use uuid::Uuid; @@ -1542,7 +1542,7 @@ impl Workspace { if let Some(active_panel) = dock.active_panel() { if was_visible { - if active_panel.has_focus(cx) { + if active_panel.focus_handle(cx).contains_focused(cx) { focus_center = true; } } else { @@ -1589,7 +1589,9 @@ impl Workspace { /// Focus the panel of the given type if it isn't already focused. If it is /// already focused, then transfer focus back to the workspace center. pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { - self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); + self.focus_or_unfocus_panel::(cx, |panel, cx| { + !panel.focus_handle(cx).contains_focused(cx) + }); } /// Focus or unfocus the given panel type, depending on the given callback. @@ -1681,7 +1683,7 @@ impl Workspace { if Some(dock.position()) != dock_to_reveal { if let Some(panel) = dock.active_panel() { if panel.is_zoomed(cx) { - focus_center |= panel.has_focus(cx); + focus_center |= panel.focus_handle(cx).contains_focused(cx); dock.set_open(false, cx); } } From dffe0ea058a59cbcd0dd99dfb41bbaf6f4511d4f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 09:23:24 -0700 Subject: [PATCH 17/90] Reintroduce menu-related platform callbacks --- crates/gpui2/src/platform.rs | 6 +- crates/gpui2/src/platform/mac/platform.rs | 114 ++++++++++----------- crates/gpui2/src/platform/test/platform.rs | 12 +++ 3 files changed, 73 insertions(+), 59 deletions(-) diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 651392c9c80eef548fb15a3c2d88d33488740661..40c555301bdf00cbad7d2ef36b2c99511daa6d90 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -5,7 +5,7 @@ mod mac; mod test; use crate::{ - point, size, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, + point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size, TaskLabel, @@ -90,6 +90,10 @@ pub trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); + fn on_menu_command(&self, callback: Box); + fn on_will_open_menu(&self, callback: Box); + fn on_validate_menu_command(&self, callback: Box bool>); + fn os_name(&self) -> &'static str; fn os_version(&self) -> Result; fn app_version(&self) -> Result; diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 314f055811c57cde9c654294e2d39ed7f1cc3806..9d02c8fb938e9b6f4cd1014e5ebce4f037911bcd 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,9 +1,9 @@ use super::BoolExt; use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, - InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, - PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, - SemanticVersion, VideoTimestamp, WindowOptions, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, + MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, + Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; @@ -155,12 +155,12 @@ pub struct MacPlatformState { reopen: Option>, quit: Option>, event: Option bool>>, - // menu_command: Option>, - // validate_menu_command: Option bool>>, + menu_command: Option>, + validate_menu_command: Option bool>>, will_open_menu: Option>, + menu_actions: Vec>, open_urls: Option)>>, finish_launching: Option>, - // menu_actions: Vec>, } impl MacPlatform { @@ -179,12 +179,12 @@ impl MacPlatform { reopen: None, quit: None, event: None, + menu_command: None, + validate_menu_command: None, will_open_menu: None, + menu_actions: Default::default(), open_urls: None, finish_launching: None, - // menu_command: None, - // validate_menu_command: None, - // menu_actions: Default::default(), })) } @@ -681,17 +681,17 @@ impl Platform for MacPlatform { } } - // fn on_menu_command(&self, callback: Box) { - // self.0.lock().menu_command = Some(callback); - // } + fn on_menu_command(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } - // fn on_will_open_menu(&self, callback: Box) { - // self.0.lock().will_open_menu = Some(callback); - // } + fn on_will_open_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } - // fn on_validate_menu_command(&self, callback: Box bool>) { - // self.0.lock().validate_menu_command = Some(callback); - // } + fn on_validate_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { // unsafe { @@ -956,7 +956,7 @@ unsafe fn path_from_objc(path: id) -> PathBuf { PathBuf::from(path) } -unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform { +unsafe fn get_mac_platform(object: &mut Object) -> &MacPlatform { let platform_ptr: *mut c_void = *object.get_ivar(MAC_PLATFORM_IVAR); assert!(!platform_ptr.is_null()); &*(platform_ptr as *const MacPlatform) @@ -965,7 +965,7 @@ unsafe fn get_foreground_platform(object: &mut Object) -> &MacPlatform { extern "C" fn send_event(this: &mut Object, _sel: Sel, native_event: id) { unsafe { if let Some(event) = InputEvent::from_native(native_event, None) { - let platform = get_foreground_platform(this); + let platform = get_mac_platform(this); if let Some(callback) = platform.0.lock().event.as_mut() { if !callback(event) { return; @@ -981,7 +981,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { let app: id = msg_send![APP_CLASS, sharedApplication]; app.setActivationPolicy_(NSApplicationActivationPolicyRegular); - let platform = get_foreground_platform(this); + let platform = get_mac_platform(this); let callback = platform.0.lock().finish_launching.take(); if let Some(callback) = callback { callback(); @@ -991,7 +991,7 @@ extern "C" fn did_finish_launching(this: &mut Object, _: Sel, _: id) { extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_windows: bool) { if !has_open_windows { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().reopen.as_mut() { callback(); } @@ -999,21 +999,21 @@ extern "C" fn should_handle_reopen(this: &mut Object, _: Sel, _: id, has_open_wi } extern "C" fn did_become_active(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().become_active.as_mut() { callback(); } } extern "C" fn did_resign_active(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().resign_active.as_mut() { callback(); } } extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) { - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().quit.as_mut() { callback(); } @@ -1035,49 +1035,47 @@ extern "C" fn open_urls(this: &mut Object, _: Sel, _: id, urls: id) { }) .collect::>() }; - let platform = unsafe { get_foreground_platform(this) }; + let platform = unsafe { get_mac_platform(this) }; if let Some(callback) = platform.0.lock().open_urls.as_mut() { callback(urls); } } -extern "C" fn handle_menu_item(__this: &mut Object, _: Sel, __item: id) { - todo!() - // unsafe { - // let platform = get_foreground_platform(this); - // let mut platform = platform.0.lock(); - // if let Some(mut callback) = platform.menu_command.take() { - // let tag: NSInteger = msg_send![item, tag]; - // let index = tag as usize; - // if let Some(action) = platform.menu_actions.get(index) { - // callback(action.as_ref()); - // } - // platform.menu_command = Some(callback); - // } - // } +extern "C" fn handle_menu_item(this: &mut Object, _: Sel, item: id) { + unsafe { + let platform = get_mac_platform(this); + let mut platform = platform.0.lock(); + if let Some(mut callback) = platform.menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = platform.menu_actions.get(index) { + callback(action.as_ref()); + } + platform.menu_command = Some(callback); + } + } } -extern "C" fn validate_menu_item(__this: &mut Object, _: Sel, __item: id) -> bool { - todo!() - // unsafe { - // let mut result = false; - // let platform = get_foreground_platform(this); - // let mut platform = platform.0.lock(); - // if let Some(mut callback) = platform.validate_menu_command.take() { - // let tag: NSInteger = msg_send![item, tag]; - // let index = tag as usize; - // if let Some(action) = platform.menu_actions.get(index) { - // result = callback(action.as_ref()); - // } - // platform.validate_menu_command = Some(callback); - // } - // result - // } +extern "C" fn validate_menu_item(this: &mut Object, _: Sel, item: id) -> bool { + unsafe { + let mut result = false; + let platform = get_mac_platform(this); + let mut platform = platform.0.lock(); + if let Some(mut callback) = platform.validate_menu_command.take() { + let tag: NSInteger = msg_send![item, tag]; + let index = tag as usize; + if let Some(action) = platform.menu_actions.get(index) { + result = callback(action.as_ref()); + } + platform.validate_menu_command = Some(callback); + } + result + } } extern "C" fn menu_will_open(this: &mut Object, _: Sel, _: id) { unsafe { - let platform = get_foreground_platform(this); + let platform = get_mac_platform(this); let mut platform = platform.0.lock(); if let Some(mut callback) = platform.will_open_menu.take() { callback(); diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index fa4b6e18c587521d88d8d4a4fd4041952c584f4a..6fa706f617119bfad728078a378309c16ab19df4 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -205,6 +205,18 @@ impl Platform for TestPlatform { unimplemented!() } + fn on_menu_command(&self, _callback: Box) { + unimplemented!() + } + + fn on_will_open_menu(&self, _callback: Box) { + unimplemented!() + } + + fn on_validate_menu_command(&self, _callback: Box bool>) { + unimplemented!() + } + fn os_name(&self) -> &'static str { "test" } From 412c6157b108a250cf1c2e85eafd3f731f71f4c2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 5 Dec 2023 17:33:35 +0100 Subject: [PATCH 18/90] Port quick_action_bar to zed2 Co-authored-by: Nate --- Cargo.lock | 12 + Cargo.toml | 1 + crates/quick_action_bar2/Cargo.toml | 22 ++ .../quick_action_bar2/src/quick_action_bar.rs | 285 ++++++++++++++++++ crates/zed2/Cargo.toml | 2 +- crates/zed2/src/zed2.rs | 11 +- 6 files changed, 327 insertions(+), 6 deletions(-) create mode 100644 crates/quick_action_bar2/Cargo.toml create mode 100644 crates/quick_action_bar2/src/quick_action_bar.rs diff --git a/Cargo.lock b/Cargo.lock index 66125d770390222874f86b419bf3741147edea5c..1388b3f0536a6218445d0041afc9b3ec29362eaa 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7074,6 +7074,17 @@ dependencies = [ "workspace", ] +[[package]] +name = "quick_action_bar2" +version = "0.1.0" +dependencies = [ + "editor2", + "gpui2", + "search2", + "ui2", + "workspace2", +] + [[package]] name = "quote" version = "1.0.33" @@ -11843,6 +11854,7 @@ dependencies = [ "postage", "project2", "project_panel2", + "quick_action_bar2", "rand 0.8.5", "regex", "rope2", diff --git a/Cargo.toml b/Cargo.toml index 3658ffad297f2c9d4fb3bf5eb6b03ede591d37e6..6477e2216c775c7e1a70a26d4169931e653b5da7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -89,6 +89,7 @@ members = [ "crates/project_panel", "crates/project_panel2", "crates/project_symbols", + "crates/quick_action_bar2", "crates/recent_projects", "crates/rope", "crates/rpc", diff --git a/crates/quick_action_bar2/Cargo.toml b/crates/quick_action_bar2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..32f440d202648b5c0dba8071d7aa5e49d4da18db --- /dev/null +++ b/crates/quick_action_bar2/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "quick_action_bar2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/quick_action_bar.rs" +doctest = false + +[dependencies] +#assistant = { path = "../assistant" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +search = { package = "search2", path = "../search2" } +workspace = { package = "workspace2", path = "../workspace2" } +ui = { package = "ui2", path = "../ui2" } + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs new file mode 100644 index 0000000000000000000000000000000000000000..6b8f15d4c95f361ab2e881ab32174b474f16713a --- /dev/null +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -0,0 +1,285 @@ +// use assistant::{assistant_panel::InlineAssist, AssistantPanel}; +use editor::Editor; + +use gpui::{ + Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, + Styled, Subscription, View, ViewContext, WeakView, +}; +use search::{buffer_search, BufferSearchBar}; +use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; +use workspace::{ + item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +pub struct QuickActionBar { + buffer_search_bar: View, + active_item: Option>, + _inlay_hints_enabled_subscription: Option, + workspace: WeakView, +} + +impl QuickActionBar { + pub fn new(buffer_search_bar: View, workspace: &Workspace) -> Self { + Self { + buffer_search_bar, + active_item: None, + _inlay_hints_enabled_subscription: None, + workspace: workspace.weak_handle(), + } + } + + fn active_editor(&self) -> Option> { + self.active_item + .as_ref() + .and_then(|item| item.downcast::()) + } +} + +impl Render for QuickActionBar { + type Element = Stateful
; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let search_button = QuickActionBarButton::new( + "toggle buffer search", + Icon::MagnifyingGlass, + !self.buffer_search_bar.read(cx).is_dismissed(), + Box::new(search::buffer_search::Deploy { focus: false }), + "Buffer Search", + ); + let assistant_button = QuickActionBarButton::new( + "toggle inline assitant", + Icon::MagicWand, + false, + Box::new(gpui::NoAction), + "Inline assistant", + ); + h_stack() + .id("quick action bar") + .p_1() + .gap_2() + .child(search_button) + .child( + div() + .border() + .border_color(gpui::red()) + .child(assistant_button), + ) + } +} + +impl EventEmitter for QuickActionBar {} + +// impl View for QuickActionBar { +// fn ui_name() -> &'static str { +// "QuickActionsBar" +// } + +// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { +// let Some(editor) = self.active_editor() else { +// return div(); +// }; + +// let mut bar = Flex::row(); +// if editor.read(cx).supports_inlay_hints(cx) { +// bar = bar.with_child(render_quick_action_bar_button( +// 0, +// "icons/inlay_hint.svg", +// editor.read(cx).inlay_hints_enabled(), +// ( +// "Toggle Inlay Hints".to_string(), +// Some(Box::new(editor::ToggleInlayHints)), +// ), +// cx, +// |this, cx| { +// if let Some(editor) = this.active_editor() { +// editor.update(cx, |editor, cx| { +// editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); +// }); +// } +// }, +// )); +// } + +// if editor.read(cx).buffer().read(cx).is_singleton() { +// let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); +// let search_action = buffer_search::Deploy { focus: true }; + +// bar = bar.with_child(render_quick_action_bar_button( +// 1, +// "icons/magnifying_glass.svg", +// search_bar_shown, +// ( +// "Buffer Search".to_string(), +// Some(Box::new(search_action.clone())), +// ), +// cx, +// move |this, cx| { +// this.buffer_search_bar.update(cx, |buffer_search_bar, cx| { +// if search_bar_shown { +// buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); +// } else { +// buffer_search_bar.deploy(&search_action, cx); +// } +// }); +// }, +// )); +// } + +// bar.add_child(render_quick_action_bar_button( +// 2, +// "icons/magic-wand.svg", +// false, +// ("Inline Assist".into(), Some(Box::new(InlineAssist))), +// cx, +// move |this, cx| { +// if let Some(workspace) = this.workspace.upgrade(cx) { +// workspace.update(cx, |workspace, cx| { +// AssistantPanel::inline_assist(workspace, &Default::default(), cx); +// }); +// } +// }, +// )); + +// bar.into_any() +// } +// } + +#[derive(IntoElement)] +struct QuickActionBarButton { + id: ElementId, + icon: Icon, + toggled: bool, + action: Box, + tooltip: SharedString, + tooltip_meta: Option, +} + +impl QuickActionBarButton { + fn new( + id: impl Into, + icon: Icon, + toggled: bool, + action: Box, + tooltip: impl Into, + ) -> Self { + Self { + id: id.into(), + icon, + toggled, + action, + tooltip: tooltip.into(), + tooltip_meta: None, + } + } + + pub fn meta(mut self, meta: Option>) -> Self { + self.tooltip_meta = meta.map(|meta| meta.into()); + self + } +} + +impl RenderOnce for QuickActionBarButton { + type Rendered = IconButton; + + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let tooltip = self.tooltip.clone(); + let action = self.action.boxed_clone(); + let tooltip_meta = self.tooltip_meta.clone(); + + IconButton::new(self.id.clone(), self.icon) + .size(ButtonSize::Compact) + .icon_size(IconSize::Small) + .style(ButtonStyle::Subtle) + .selected(self.toggled) + .tooltip(move |cx| { + if let Some(meta) = &tooltip_meta { + Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx) + } else { + Tooltip::for_action(tooltip.clone(), &*action, cx) + } + }) + .on_click({ + let action = self.action.boxed_clone(); + move |_, cx| cx.dispatch_action(action.boxed_clone()) + }) + } +} + +// fn render_quick_action_bar_button< +// F: 'static + Fn(&mut QuickActionBar, &mut ViewContext), +// >( +// index: usize, +// icon: &'static str, +// toggled: bool, +// tooltip: (String, Option>), +// cx: &mut ViewContext, +// on_click: F, +// ) -> AnyElement { +// enum QuickActionBarButton {} + +// let theme = theme::current(cx); +// let (tooltip_text, action) = tooltip; + +// MouseEventHandler::new::(index, cx, |mouse_state, _| { +// let style = theme +// .workspace +// .toolbar +// .toggleable_tool +// .in_state(toggled) +// .style_for(mouse_state); +// Svg::new(icon) +// .with_color(style.color) +// .constrained() +// .with_width(style.icon_width) +// .aligned() +// .constrained() +// .with_width(style.button_width) +// .with_height(style.button_width) +// .contained() +// .with_style(style.container) +// }) +// .with_cursor_style(CursorStyle::PointingHand) +// .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) +// .with_tooltip::(index, tooltip_text, action, theme.tooltip.clone(), cx) +// .into_any_named("quick action bar button") +// } + +impl ToolbarItemView for QuickActionBar { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + match active_pane_item { + Some(active_item) => { + self.active_item = Some(active_item.boxed_clone()); + self._inlay_hints_enabled_subscription.take(); + + if let Some(editor) = active_item.downcast::() { + let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); + let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); + self._inlay_hints_enabled_subscription = + Some(cx.observe(&editor, move |_, editor, cx| { + let editor = editor.read(cx); + let new_inlay_hints_enabled = editor.inlay_hints_enabled(); + let new_supports_inlay_hints = editor.supports_inlay_hints(cx); + let should_notify = inlay_hints_enabled != new_inlay_hints_enabled + || supports_inlay_hints != new_supports_inlay_hints; + inlay_hints_enabled = new_inlay_hints_enabled; + supports_inlay_hints = new_supports_inlay_hints; + if should_notify { + cx.notify() + } + })); + ToolbarItemLocation::PrimaryRight + } else { + ToolbarItemLocation::Hidden + } + } + None => { + self.active_item = None; + ToolbarItemLocation::Hidden + } + } + } +} diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index ee9416e234188400164f2ebd1f713bc145364862..cc6150676438fef760aa70913651695968380e2d 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" } project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } -# quick_action_bar = { path = "../quick_action_bar" } +quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } # recent_projects = { path = "../recent_projects" } rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 1b9f1cc719bc889377ba113a78b09a72d91656a1..abd6b16e3d376c1c690b0851a1db722dbd345e19 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -19,6 +19,7 @@ pub use open_listener::*; use anyhow::{anyhow, Context as _}; use project_panel::ProjectPanel; +use quick_action_bar::QuickActionBar; use settings::{initial_local_settings_content, Settings}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; @@ -100,11 +101,11 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { toolbar.add_item(breadcrumbs, cx); let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - // todo!() - // let quick_action_bar = cx.add_view(|_| { - // QuickActionBar::new(buffer_search_bar, workspace) - // }); - // toolbar.add_item(quick_action_bar, cx); + + let quick_action_bar = cx.build_view(|_| { + QuickActionBar::new(buffer_search_bar, workspace) + }); + toolbar.add_item(quick_action_bar, cx); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); From eed5a698cff8d19614e9bc07c5e656e34703ec26 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 11:59:23 -0500 Subject: [PATCH 19/90] Update tab close button --- crates/workspace2/src/pane.rs | 11 +++++------ crates/workspace2/src/toolbar.rs | 30 +----------------------------- 2 files changed, 6 insertions(+), 35 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 2433edee0efe25cf7125559ef9f6e364c69b4681..599c1d88c78d48dcb31a5784ab44afa3d74015c9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1421,13 +1421,12 @@ impl Pane { let close_icon = || { let id = item.item_id(); - div() + h_stack() .id(ix) - .w_3p5() - .h_3p5() - .rounded_sm() - .border() - .border_color(cx.theme().colors().border_variant) + .justify_center() + .w_4() + .h_4() + .rounded_md() .absolute() .map(|this| { if close_right { diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index d7cb741791789b2276b64892930352e777ef14e1..d47e99cb2027bfea51775e2060a4ea948342d0e7 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -87,35 +87,7 @@ impl Render for Toolbar { .child( h_stack() .justify_between() - // Toolbar left side - .children(self.items.iter().map(|(child, _)| child.to_any())) - // Toolbar right side - .child( - h_stack() - .p_1() - .gap_2() - .child( - IconButton::new("toggle-inlay-hints", Icon::InlayHint) - .size(ui::ButtonSize::Compact) - .icon_size(ui::IconSize::Small) - .style(ui::ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Inlay Hints", cx)), - ) - .child( - IconButton::new("buffer-search", Icon::MagnifyingGlass) - .size(ui::ButtonSize::Compact) - .icon_size(ui::IconSize::Small) - .style(ui::ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Search in File", cx)), - ) - .child( - IconButton::new("inline-assist", Icon::MagicWand) - .size(ui::ButtonSize::Compact) - .icon_size(ui::IconSize::Small) - .style(ui::ButtonStyle::Subtle) - .tooltip(move |cx| Tooltip::text("Inline Assist", cx)), - ), - ), + .children(self.items.iter().map(|(child, _)| child.to_any())), ) } } From e534c5fdcd18c7003502dc8a31e8ff4555fec5c8 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 18:14:24 +0100 Subject: [PATCH 20/90] WIP --- crates/assistant2/src/assistant_panel.rs | 503 ++++++++--------------- crates/ui2/src/components/icon.rs | 2 + 2 files changed, 167 insertions(+), 338 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index f3bd06328d3e42760b0cc70cc0543f1c3cbfcdaa..a6a04421b0bd15cae36b5a96d426e50e78ffd5d7 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -27,9 +27,10 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - actions, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, ClipboardItem, - Div, Element, Entity, EventEmitter, FocusHandle, FocusableView, HighlightStyle, - InteractiveElement, IntoElement, Model, ModelContext, Render, Styled, Subscription, Task, + actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, + ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView, + HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, + PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; @@ -48,7 +49,10 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use ui::{h_stack, v_stack, ButtonCommon, ButtonLike, Clickable, IconButton, Label}; +use ui::{ + h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, + IconElement, Label, Selectable, StyledExt, Tooltip, +}; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; use workspace::{ @@ -958,7 +962,7 @@ impl AssistantPanel { } fn render_hamburger_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("hamburger_button", ui::Icon::Menu) + IconButton::new("hamburger_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if this.active_editor().is_some() { this.set_active_editor_index(None, cx); @@ -966,7 +970,7 @@ impl AssistantPanel { this.set_active_editor_index(this.prev_active_editor_index, cx); } })) - .tooltip(|cx| ui::Tooltip::text("History", cx)) + .tooltip(|cx| Tooltip::text("History", cx)) } fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { @@ -982,27 +986,27 @@ impl AssistantPanel { } fn render_split_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("split_button", ui::Icon::Menu) + IconButton::new("split_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx)); } })) - .tooltip(|cx| ui::Tooltip::for_action("Split Message", &Split, cx)) + .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx)) } fn render_assist_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("assist_button", ui::Icon::Menu) + IconButton::new("assist_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if let Some(active_editor) = this.active_editor() { active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx)); } })) - .tooltip(|cx| ui::Tooltip::for_action("Assist", &Assist, cx)) + .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx)) } fn render_quote_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("quote_button", ui::Icon::Menu) + IconButton::new("quote_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { if let Some(workspace) = this.workspace.upgrade() { cx.window_context().defer(move |cx| { @@ -1012,24 +1016,24 @@ impl AssistantPanel { }); } })) - .tooltip(|cx| ui::Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) + .tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx)) } fn render_plus_button(cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("plus_button", ui::Icon::Menu) + IconButton::new("plus_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { this.new_conversation(cx); })) - .tooltip(|cx| ui::Tooltip::for_action("New Conversation", &NewConversation, cx)) + .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx)) } fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { - IconButton::new("zoom_button", ui::Icon::Menu) + IconButton::new("zoom_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { this.toggle_zoom(&ToggleZoom, cx); })) .tooltip(|cx| { - ui::Tooltip::for_action( + Tooltip::for_action( if self.zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx, @@ -1111,9 +1115,9 @@ fn build_api_key_editor(cx: &mut ViewContext) -> View { } impl Render for AssistantPanel { - type Element = Div; + type Element = Focusable
; - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { if let Some(api_key_editor) = self.api_key_editor.clone() { v_stack() .track_focus(&self.focus_handle) @@ -1249,8 +1253,8 @@ impl Panel for AssistantPanel { } } - fn icon(&self, cx: &WindowContext) -> Option { - Some(ui::Icon::Ai) + fn icon(&self, cx: &WindowContext) -> Option { + Some(Icon::Ai) } fn toggle_action(&self) -> Box { @@ -2052,6 +2056,7 @@ struct ConversationEditor { editor: View, blocks: HashSet, scroll_position: Option, + focus_handle: FocusHandle, _subscriptions: Vec, } @@ -2082,10 +2087,13 @@ impl ConversationEditor { editor }); + let focus_handle = cx.focus_handle(); + let _subscriptions = vec![ cx.observe(&conversation, |_, _, cx| cx.notify()), cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&editor, Self::handle_editor_event), + cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)), ]; let mut this = Self { @@ -2095,6 +2103,7 @@ impl ConversationEditor { scroll_position: None, fs, workspace, + focus_handle, _subscriptions, }; this.update_message_headers(cx); @@ -2265,88 +2274,47 @@ impl ConversationEditor { style: BlockStyle::Sticky, render: Arc::new({ let conversation = self.conversation.clone(); - // let metadata = message.metadata.clone(); - // let message = message.clone(); move |cx| { - enum Sender {} - enum ErrorTooltip {} - let message_id = message.id; - let sender = MouseEventHandler::new::( - message_id.0, - cx, - |state, _| match message.role { - Role::User => { - let style = style.user_sender.style_for(state); - Label::new("You", style.text.clone()) - .contained() - .with_style(style.container) - } + let sender = ButtonLike::new("role") + .child(match message.role { + Role::User => Label::new("You").color(Color::Default), Role::Assistant => { - let style = style.assistant_sender.style_for(state); - Label::new("Assistant", style.text.clone()) - .contained() - .with_style(style.container) + Label::new("Assistant").color(Color::Modified) } - Role::System => { - let style = style.system_sender.style_for(state); - Label::new("System", style.text.clone()) - .contained() - .with_style(style.container) + Role::System => Label::new("System").color(Color::Warning), + }) + .on_click({ + let conversation = conversation.clone(); + move |_, _, cx| { + conversation.update(cx, |conversation, cx| { + conversation.cycle_message_roles( + HashSet::from_iter(Some(message_id)), + cx, + ) + }) } - }, - ) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, { - let conversation = conversation.clone(); - move |_, _, cx| { - conversation.update(cx, |conversation, cx| { - conversation.cycle_message_roles( - HashSet::from_iter(Some(message_id)), - cx, - ) - }) - } - }); + }); - Flex::row() - .with_child(sender.aligned()) - .with_child( - Label::new( - message.sent_at.format("%I:%M%P").to_string(), - style.sent_at.text.clone(), - ) - .contained() - .with_style(style.sent_at.container) - .aligned(), - ) + h_stack() + .id(("message_header", message_id.0)) + .border() + .border_color(gpui::red()) + .child(sender) + .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) .with_children( if let MessageStatus::Error(error) = &message.status { Some( - Svg::new("icons/error.svg") - .with_color(style.error_icon.color) - .constrained() - .with_width(style.error_icon.width) - .contained() - .with_style(style.error_icon.container) - .with_tooltip::( - message_id.0, - error.to_string(), - None, - theme.tooltip.clone(), - cx, - ) - .aligned(), + div() + .id("error") + .tooltip(|cx| Tooltip::text(error, cx)) + .child(IconElement::new(Icon::XCircle)), ) } else { None }, ) - .aligned() - .left() - .contained() - .with_style(style.message_header) - .into_any() + .into_any_element() } }), disposition: BlockDisposition::Above, @@ -2491,78 +2459,48 @@ impl ConversationEditor { .unwrap_or_else(|| "New Conversation".into()) } - fn render_current_model( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> impl Element { - enum Model {} - - MouseEventHandler::new::(0, cx, |state, cx| { - let style = style.model.style_for(state); - let model_display_name = self.conversation.read(cx).model.short_name(); - Label::new(model_display_name, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)) + fn render_current_model(&self, cx: &mut ViewContext) -> impl IntoElement { + Button::new( + "current_model", + self.conversation.read(cx).model.short_name(), + ) + .tooltip(move |cx| Tooltip::text("Change Model", cx)) + .on_click(cx.listener(|this, _, cx| this.cycle_model(cx))) } - fn render_remaining_tokens( - &self, - style: &AssistantStyle, - cx: &mut ViewContext, - ) -> Option> { + fn render_remaining_tokens(&self, cx: &mut ViewContext) -> Option { let remaining_tokens = self.conversation.read(cx).remaining_tokens()?; - let remaining_tokens_style = if remaining_tokens <= 0 { - &style.no_remaining_tokens + let remaining_tokens_color = if remaining_tokens <= 0 { + Color::Error } else if remaining_tokens <= 500 { - &style.low_remaining_tokens + Color::Warning } else { - &style.remaining_tokens + Color::Default }; Some( - Label::new( - remaining_tokens.to_string(), - remaining_tokens_style.text.clone(), - ) - .contained() - .with_style(remaining_tokens_style.container), + div() + .border() + .border_color(gpui::red()) + .child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)), ) } } impl EventEmitter for ConversationEditor {} -impl View for ConversationEditor { - fn ui_name() -> &'static str { - "ConversationEditor" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).assistant; - Stack::new() - .with_child( - ChildView::new(&self.editor, cx) - .contained() - .with_style(theme.container), - ) - .with_child( - Flex::row() - .with_child(self.render_current_model(theme, cx)) - .with_children(self.render_remaining_tokens(theme, cx)) - .aligned() - .top() - .right(), - ) - .into_any() - } +impl Render for ConversationEditor { + type Element = Div; - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.editor); - } + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().relative().child(self.editor.clone()).child( + h_stack() + .absolute() + .gap_1() + .top_3() + .right_5() + .child(self.render_current_model(cx)) + .children(self.render_remaining_tokens(cx)), + ) } } @@ -2616,7 +2554,7 @@ struct InlineAssistant { prompt_editor: View, workspace: WeakView, confirmed: bool, - has_focus: bool, + focus_handle: FocusHandle, include_conversation: bool, measurements: Rc>, prompt_history: VecDeque, @@ -2631,124 +2569,63 @@ struct InlineAssistant { maintain_rate_limit: Option>, } -impl Entity for InlineAssistant { - type Event = InlineAssistantEvent; -} +impl EventEmitter for InlineAssistant {} -impl View for InlineAssistant { - fn ui_name() -> &'static str { - "InlineAssistant" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum ErrorIcon {} - let theme = theme::current(cx); - - Flex::row() - .with_children([Flex::row() - .with_child( - Button::action(ToggleIncludeConversation) - .with_tooltip("Include Conversation", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new("icons/ai.svg")) - .toggleable(self.include_conversation) - .with_style(theme.assistant.inline.include_conversation.clone()) - .element() - .aligned(), - ) - .with_children(if SemanticIndex::enabled(cx) { - Some( - Button::action(ToggleRetrieveContext) - .with_tooltip("Retrieve Context", theme.tooltip.clone()) - .with_id(self.id) - .with_contents(theme::components::svg::Svg::new( - "icons/magnifying_glass.svg", - )) - .toggleable(self.retrieve_context) - .with_style(theme.assistant.inline.retrieve_context.clone()) - .element() - .aligned(), - ) - } else { - None - }) - .with_children(if let Some(error) = self.codegen.read(cx).error() { - Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - error.to_string(), - None, - theme.tooltip.clone(), +impl Render for InlineAssistant { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let measurements = self.measurements.get(); + h_stack() + .child( + h_stack() + .justify_center() + .w(measurements.gutter_width) + .child( + IconButton::new("include_conversation", Icon::Ai) + .action(ToggleIncludeConversation) + .selected(self.include_conversation) + .tooltip(Tooltip::for_action( + "Include Conversation", + &ToggleIncludeConversation, cx, - ) - .aligned(), + )), ) - } else { - None - }) - .aligned() - .constrained() - .dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f(measurements.gutter_width, constraint.min.y()), - max: vec2f(measurements.gutter_width, constraint.max.y()), - } - } - })]) - .with_child(Empty::new().constrained().dynamically({ - let measurements = self.measurements.clone(); - move |constraint, _, _| { - let measurements = measurements.get(); - SizeConstraint { - min: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.min.y(), - ), - max: vec2f( - measurements.anchor_x - measurements.gutter_width, - constraint.max.y(), - ), - } - } - })) - .with_child( - ChildView::new(&self.prompt_editor, cx) - .aligned() - .left() - .flex(1., true), + .children(if SemanticIndex::enabled(cx) { + Some( + IconButton::new("retrieve_context", Icon::MagnifyingGlass) + .action(ToggleRetrieveContext) + .selected(self.retrieve_context) + .tooltip(Tooltip::for_action( + "Retrieve Context", + &ToggleRetrieveContext, + cx, + )), + ) + } else { + None + }) + .children(if let Some(error) = self.codegen.read(cx).error() { + Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text(error.to_string(), cx)) + .child(IconElement::new(Icon::XCircle).color(Color::Error)), + ) + } else { + None + }), ) - .with_children(if self.retrieve_context { - Some( - Flex::row() - .with_children(self.retrieve_context_status(cx)) - .flex(1., true) - .aligned(), - ) + .child( + div() + .ml(measurements.anchor_x - measurements.gutter_width) + .child(self.prompt_editor.clone()), + ) + .children(if self.retrieve_context { + self.retrieve_context_status(cx) } else { None }) - .contained() - .with_style(theme.assistant.inline.container) - .into_any() - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - cx.focus(&self.prompt_editor); - self.has_focus = true; - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; } } @@ -2765,11 +2642,8 @@ impl InlineAssistant { semantic_index: Option>, project: Model, ) -> Self { - let prompt_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); + let prompt_editor = cx.build_view(|cx| { + let mut editor = Editor::single_line(cx); let placeholder = match codegen.read(cx).kind() { CodegenKind::Transform { .. } => "Enter transformation prompt…", CodegenKind::Generate { .. } => "Enter generation prompt…", @@ -2777,9 +2651,15 @@ impl InlineAssistant { editor.set_placeholder_text(placeholder, cx); editor }); + + let focus_handle = cx.focus_handle(); let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), + cx.on_focus( + &focus_handle, + cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)), + ), ]; if let Some(semantic_index) = semantic_index.clone() { @@ -2791,7 +2671,7 @@ impl InlineAssistant { prompt_editor, workspace, confirmed: false, - has_focus: false, + focus_handle, include_conversation, measurements, prompt_history, @@ -3008,10 +2888,7 @@ impl InlineAssistant { anyhow::Ok(()) } - fn retrieve_context_status( - &self, - cx: &mut ViewContext, - ) -> Option> { + fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { enum ContextStatusIcon {} let Some(project) = self.project.upgrade() else { @@ -3020,47 +2897,27 @@ impl InlineAssistant { if let Some(semantic_index) = SemanticIndex::global(cx) { let status = semantic_index.update(cx, |index, _| index.status(&project)); - let theme = theme::current(cx); match status { SemanticIndexStatus::NotAuthenticated {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.error_icon.color) - .constrained() - .with_width(theme.assistant.error_icon.width) - .contained() - .with_style(theme.assistant.error_icon.container) - .with_tooltip::( - self.id, - "Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() ), + SemanticIndexStatus::NotIndexed {} => Some( - Svg::new("icons/error.svg") - .with_color(theme.assistant.inline.context_status.error_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.error_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.error_icon.container) - .with_tooltip::( - self.id, - "Not Indexed", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Indexed", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() ), + SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry, } => { - let mut status_text = if remaining_files == 0 { "Indexing...".to_string() } else { @@ -3079,6 +2936,11 @@ impl InlineAssistant { } } Some( + div() + .id("update") + .tooltip(|cx| Tooltip::text(status_text, cx)) + .child(IconElement::new(Icon::Update).color(color)) + .into_any_element() Svg::new("icons/update.svg") .with_color(theme.assistant.inline.context_status.in_progress_icon.color) .constrained() @@ -3096,6 +2958,7 @@ impl InlineAssistant { .into_any(), ) } + SemanticIndexStatus::Indexed {} => Some( Svg::new("icons/check.svg") .with_color(theme.assistant.inline.context_status.complete_icon.color) @@ -3119,42 +2982,6 @@ impl InlineAssistant { } } - // fn retrieve_context_status(&self, cx: &mut ViewContext) -> String { - // let project = self.project.clone(); - // if let Some(semantic_index) = self.semantic_index.clone() { - // let status = semantic_index.update(cx, |index, cx| index.status(&project)); - // return match status { - // // This theoretically shouldnt be a valid code path - // // As the inline assistant cant be launched without an API key - // // We keep it here for safety - // semantic_index::SemanticIndexStatus::NotAuthenticated => { - // "Not Authenticated!\nPlease ensure you have an `OPENAI_API_KEY` in your environment variables.".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexed => { - // "Indexing Complete!".to_string() - // } - // semantic_index::SemanticIndexStatus::Indexing { remaining_files, rate_limit_expiry } => { - - // let mut status = format!("Remaining files to index for Context Retrieval: {remaining_files}"); - - // if let Some(rate_limit_expiry) = rate_limit_expiry { - // let remaining_seconds = - // rate_limit_expiry.duration_since(Instant::now()); - // if remaining_seconds > Duration::from_secs(0) { - // write!(status, " (rate limit resets in {}s)", remaining_seconds.as_secs()).unwrap(); - // } - // } - // status - // } - // semantic_index::SemanticIndexStatus::NotIndexed => { - // "Not Indexed for Context Retrieval".to_string() - // } - // }; - // } - - // "".to_string() - // } - fn toggle_include_conversation( &mut self, _: &ToggleIncludeConversation, @@ -3208,8 +3035,8 @@ impl InlineAssistant { // This wouldn't need to exist if we could pass parameters when rendering child views. #[derive(Copy, Clone, Default)] struct BlockMeasurements { - anchor_x: f32, - gutter_width: f32, + anchor_x: Pixels, + gutter_width: Pixels, } struct PendingInlineAssist { diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a993a54e15463d14cbdf8c14325aec96480204e6..29e743eace6fcea7274be0f7bc85081a8672e4d3 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -81,6 +81,7 @@ pub enum Icon { Shift, Option, Return, + Update, } impl Icon { @@ -155,6 +156,7 @@ impl Icon { Icon::Shift => "icons/shift.svg", Icon::Option => "icons/option.svg", Icon::Return => "icons/return.svg", + Icon::Update => "icons/update.svg", } } } From dccdcd322101461fbdd83d83bf1736a3bd01b47c Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 12:41:54 -0500 Subject: [PATCH 21/90] Add indicator component Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/ui2/src/components.rs | 2 + crates/ui2/src/components/indicator.rs | 59 ++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 crates/ui2/src/components/indicator.rs diff --git a/crates/ui2/src/components.rs b/crates/ui2/src/components.rs index 17271de48d4993c111c5cececd50499c6ef801b3..583b30a2e05fd647d7603d41ee4f0edbf8510713 100644 --- a/crates/ui2/src/components.rs +++ b/crates/ui2/src/components.rs @@ -5,6 +5,7 @@ mod context_menu; mod disclosure; mod divider; mod icon; +mod indicator; mod keybinding; mod label; mod list; @@ -24,6 +25,7 @@ pub use context_menu::*; pub use disclosure::*; pub use divider::*; pub use icon::*; +pub use indicator::*; pub use keybinding::*; pub use label::*; pub use list::*; diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs new file mode 100644 index 0000000000000000000000000000000000000000..af62f9d989c12f1420afe5d054a7305cfd6e3a2b --- /dev/null +++ b/crates/ui2/src/components/indicator.rs @@ -0,0 +1,59 @@ +use gpui::{AnyView, Div, Position}; + +use crate::prelude::*; + +#[derive(Default)] +pub enum IndicatorStyle { + #[default] + Dot, + Bar, +} + +#[derive(IntoElement)] +pub struct Indicator { + position: Position, + style: IndicatorStyle, + color: Color, +} + +impl Indicator { + pub fn dot() -> Self { + Self { + position: Position::Relative, + style: IndicatorStyle::Dot, + color: Color::Default, + } + } + + pub fn bar() -> Self { + Self { + position: Position::Relative, + style: IndicatorStyle::Dot, + color: Color::Default, + } + } + + pub fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + pub fn absolute(mut self) -> Self { + self.position = Position::Absolute; + self + } +} + +impl RenderOnce for Indicator { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .map(|this| match self.style { + IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(), + IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(), + }) + .when(self.position == Position::Absolute, |this| this.absolute()) + .bg(self.color.color(cx)) + } +} From 5e79807f6fc779f051b374744bbe433ab8c28dad Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:14:40 -0800 Subject: [PATCH 22/90] Fix tree branch rendering in collab panel --- crates/collab_ui2/src/collab_panel.rs | 37 ++++++++++----------------- crates/gpui2/src/elements/canvas.rs | 10 +++++--- 2 files changed, 20 insertions(+), 27 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index c55bfa8cf5a7beec1c516598f242fcfa2fefe018..1de95f64b74de48b61b6a7ad0d01985afa45529b 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -169,7 +169,7 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, Action, + actions, canvas, div, img, overlay, point, prelude::*, px, rems, serde_json, size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce, @@ -1204,14 +1204,9 @@ impl CollabPanel { .detach_and_log_err(cx); }); })) - .left_child(IconButton::new(0, Icon::Folder)) - .child( - h_stack() - .w_full() - .justify_between() - .child(render_tree_branch(is_last, cx)) - .child(Label::new(project_name.clone())), - ) + .left_child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Folder)) + .child(Label::new(project_name.clone())) .tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx)) // enum JoinProject {} @@ -3119,30 +3114,24 @@ impl CollabPanel { } fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement { - let text_style = cx.text_style(); let rem_size = cx.rem_size(); - let text_system = cx.text_system(); - let font_id = text_system.font_id(&text_style.font()).unwrap(); - let font_size = text_style.font_size.to_pixels(rem_size); - let line_height = text_style.line_height_in_pixels(rem_size); - let cap_height = text_system.cap_height(font_id, font_size); - let baseline_offset = text_system.baseline_offset(font_id, font_size, line_height); - let width = cx.rem_size() * 2.5; + let line_height = cx.text_style().line_height_in_pixels(rem_size); + let width = rem_size * 1.5; let thickness = px(2.); let color = cx.theme().colors().text; canvas(move |bounds, cx| { - let start_x = bounds.left() + (bounds.size.width / 2.) - (width / 2.); - let end_x = bounds.right(); - let start_y = bounds.top(); - let end_y = bounds.top() + baseline_offset - (cap_height / 2.); + let start_x = (bounds.left() + bounds.right() - thickness) / 2.; + let start_y = (bounds.top() + bounds.bottom() - thickness) / 2.; + let right = bounds.right(); + let top = bounds.top(); cx.paint_quad( Bounds::from_corners( - point(start_x, start_y), + point(start_x, top), point( start_x + thickness, - if is_last { end_y } else { bounds.bottom() }, + if is_last { start_y } else { bounds.bottom() }, ), ), Default::default(), @@ -3151,7 +3140,7 @@ fn render_tree_branch(is_last: bool, cx: &mut WindowContext) -> impl IntoElement Hsla::transparent_black(), ); cx.paint_quad( - Bounds::from_corners(point(start_x, end_y), point(end_x, end_y + thickness)), + Bounds::from_corners(point(start_x, start_y), point(right, start_y + thickness)), Default::default(), color, Default::default(), diff --git a/crates/gpui2/src/elements/canvas.rs b/crates/gpui2/src/elements/canvas.rs index 4761b04f3f84abae558038b6830d709deb06532e..287a3b4b5a38fdc0c7c90c75763bb9a0921dfb7e 100644 --- a/crates/gpui2/src/elements/canvas.rs +++ b/crates/gpui2/src/elements/canvas.rs @@ -1,9 +1,11 @@ -use crate::{Bounds, Element, IntoElement, Pixels, StyleRefinement, Styled, WindowContext}; +use refineable::Refineable as _; + +use crate::{Bounds, Element, IntoElement, Pixels, Style, StyleRefinement, Styled, WindowContext}; pub fn canvas(callback: impl 'static + FnOnce(Bounds, &mut WindowContext)) -> Canvas { Canvas { paint_callback: Box::new(callback), - style: Default::default(), + style: StyleRefinement::default(), } } @@ -32,7 +34,9 @@ impl Element for Canvas { _: Option, cx: &mut WindowContext, ) -> (crate::LayoutId, Self::State) { - let layout_id = cx.request_layout(&self.style.clone().into(), []); + let mut style = Style::default(); + style.refine(&self.style); + let layout_id = cx.request_layout(&style, []); (layout_id, ()) } From d86da04584f2336f5d7a177e7a6db63d7e137f8c Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 5 Dec 2023 19:27:15 +0100 Subject: [PATCH 23/90] WIP --- .../ai2/src/providers/open_ai/completion.rs | 6 +- crates/assistant2/src/assistant_panel.rs | 521 +++++++++--------- crates/editor2/src/editor.rs | 93 ++-- crates/gpui2/src/window.rs | 6 + crates/util/src/arc_cow.rs | 8 +- 5 files changed, 307 insertions(+), 327 deletions(-) diff --git a/crates/ai2/src/providers/open_ai/completion.rs b/crates/ai2/src/providers/open_ai/completion.rs index 3e49fc5290e438bdad69cb01607e86a9791c4178..c9a2abd0c8c3bc170ff39f16294f2fb976f00465 100644 --- a/crates/ai2/src/providers/open_ai/completion.rs +++ b/crates/ai2/src/providers/open_ai/completion.rs @@ -104,7 +104,7 @@ pub struct OpenAIResponseStreamEvent { pub async fn stream_completion( credential: ProviderCredential, - executor: Arc, + executor: BackgroundExecutor, request: Box, ) -> Result>> { let api_key = match credential { @@ -197,11 +197,11 @@ pub async fn stream_completion( pub struct OpenAICompletionProvider { model: OpenAILanguageModel, credential: Arc>, - executor: Arc, + executor: BackgroundExecutor, } impl OpenAICompletionProvider { - pub fn new(model_name: &str, executor: Arc) -> Self { + pub fn new(model_name: &str, executor: BackgroundExecutor) -> Self { let model = OpenAILanguageModel::load(model_name); let credential = Arc::new(RwLock::new(ProviderCredential::NoCredentials)); Self { diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index a6a04421b0bd15cae36b5a96d426e50e78ffd5d7..bcf85a6948eb8cba330a0a54bae24440b7462fff 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -27,8 +27,8 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext, - ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView, + actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, + ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, @@ -51,7 +51,7 @@ use std::{ }; use ui::{ h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, - IconElement, Label, Selectable, StyledExt, Tooltip, + IconElement, Label, Selectable, Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -76,49 +76,18 @@ actions!( pub fn init(cx: &mut AppContext) { AssistantSettings::register(cx); - cx.add_action( - |this: &mut AssistantPanel, - _: &workspace::NewFile, - cx: &mut ViewContext| { - this.new_conversation(cx); - }, - ); - cx.add_action(ConversationEditor::assist); - cx.capture_action(ConversationEditor::cancel_last_assist); - cx.capture_action(ConversationEditor::save); - cx.add_action(ConversationEditor::quote_selection); - cx.capture_action(ConversationEditor::copy); - cx.add_action(ConversationEditor::split); - cx.capture_action(ConversationEditor::cycle_message_role); - cx.add_action(AssistantPanel::save_credentials); - cx.add_action(AssistantPanel::reset_credentials); - cx.add_action(AssistantPanel::toggle_zoom); - cx.add_action(AssistantPanel::deploy); - cx.add_action(AssistantPanel::select_next_match); - cx.add_action(AssistantPanel::select_prev_match); - cx.add_action(AssistantPanel::handle_editor_cancel); - cx.add_action( - |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_panel_focus::(cx); + cx.observe_new_views( + |workspace: &mut Workspace, cx: &mut ViewContext| { + workspace + .register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }) + .register_action(AssistantPanel::inline_assist) + .register_action(AssistantPanel::cancel_last_inline_assist) + .register_action(ConversationEditor::quote_selection); }, - ); - cx.add_action(AssistantPanel::inline_assist); - cx.add_action(AssistantPanel::cancel_last_inline_assist); - cx.add_action(InlineAssistant::confirm); - cx.add_action(InlineAssistant::cancel); - cx.add_action(InlineAssistant::toggle_include_conversation); - cx.add_action(InlineAssistant::toggle_retrieve_context); - cx.add_action(InlineAssistant::move_up); - cx.add_action(InlineAssistant::move_down); -} - -#[derive(Debug)] -pub enum AssistantPanelEvent { - ZoomIn, - ZoomOut, - Focus, - Close, - DockPositionChanged, + ) + .detach(); } pub struct AssistantPanel { @@ -131,7 +100,6 @@ pub struct AssistantPanel { saved_conversations: Vec, saved_conversations_scroll_handle: UniformListScrollHandle, zoomed: bool, - // todo!("remove has_focus field") focus_handle: FocusHandle, toolbar: View, completion_provider: Arc, @@ -152,9 +120,12 @@ pub struct AssistantPanel { impl AssistantPanel { const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20; - pub fn load(workspace: WeakView, cx: AsyncAppContext) -> Task>> { + pub fn load( + workspace: WeakView, + mut cx: AsyncWindowContext, + ) -> Task>> { cx.spawn(|mut cx| async move { - let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; let saved_conversations = SavedConversationMetadata::list(fs.clone()) .await .log_err() @@ -163,7 +134,7 @@ impl AssistantPanel { // TODO: deserialize state. let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { - cx.add_view::(|cx| { + cx.build_view::(|cx| { const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100); let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move { let mut events = fs @@ -184,10 +155,10 @@ impl AssistantPanel { anyhow::Ok(()) }); - let toolbar = cx.add_view(|cx| { + let toolbar = cx.build_view(|cx| { let mut toolbar = Toolbar::new(); toolbar.set_can_navigate(false, cx); - toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx); + toolbar.add_item(cx.build_view(|cx| BufferSearchBar::new(cx)), cx); toolbar }); @@ -199,8 +170,8 @@ impl AssistantPanel { )); let focus_handle = cx.focus_handle(); - cx.on_focus_in(Self::focus_in).detach(); - cx.on_focus_out(Self::focus_out).detach(); + cx.on_focus_in(&focus_handle, Self::focus_in).detach(); + cx.on_focus_out(&focus_handle, Self::focus_out).detach(); let mut this = Self { workspace: workspace_handle, @@ -231,11 +202,11 @@ impl AssistantPanel { let mut old_dock_position = this.position(cx); this.subscriptions = - vec![cx.observe_global::(move |this, cx| { + vec![cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; - cx.emit(AssistantPanelEvent::DockPositionChanged); + cx.emit(PanelEvent::ChangePosition); } cx.notify(); })]; @@ -343,7 +314,7 @@ impl AssistantPanel { // Retrieve Credentials Authenticates the Provider provider.retrieve_credentials(cx); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx) }); @@ -353,14 +324,14 @@ impl AssistantPanel { let previously_indexed = semantic_index .update(&mut cx, |index, cx| { index.project_previously_indexed(&project, cx) - }) + })? .await .unwrap_or(false); if previously_indexed { let _ = semantic_index .update(&mut cx, |index, cx| { index.index_project(project.clone(), cx) - }) + })? .await; } anyhow::Ok(()) @@ -369,7 +340,7 @@ impl AssistantPanel { } let measurements = Rc::new(Cell::new(BlockMeasurements::default())); - let inline_assistant = cx.add_view(|cx| { + let inline_assistant = cx.build_view(|cx| { let assistant = InlineAssistant::new( inline_assist_id, measurements.clone(), @@ -382,7 +353,7 @@ impl AssistantPanel { self.semantic_index.clone(), project.clone(), ); - cx.focus_self(); + assistant.focus_handle.focus(cx); assistant }); let block_id = editor.update(cx, |editor, cx| { @@ -429,8 +400,13 @@ impl AssistantPanel { move |_, editor, event, cx| { if let Some(inline_assistant) = inline_assistant.upgrade() { if let EditorEvent::SelectionsChanged { local } = event { - if *local && inline_assistant.read(cx).has_focus { - cx.focus(&editor); + if *local + && inline_assistant + .read(cx) + .focus_handle + .contains_focused(cx) + { + cx.focus_view(&editor); } } } @@ -555,7 +531,7 @@ impl AssistantPanel { } } - cx.propagate_action(); + cx.propagate(); } fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext) { @@ -709,13 +685,17 @@ impl AssistantPanel { let snippets = cx.spawn(|_, mut cx| async move { let mut snippets = Vec::new(); for result in search_results.await { - snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx)); + snippets.push(PromptCodeSnippet::new( + result.buffer, + result.range, + &mut cx, + )?); } - snippets + anyhow::Ok(snippets) }); snippets } else { - Task::ready(Vec::new()) + Task::ready(Ok(Vec::new())) }; let mut model = AssistantSettings::get_global(cx) @@ -724,7 +704,7 @@ impl AssistantPanel { let model_name = model.full_name(); let prompt = cx.background_executor().spawn(async move { - let snippets = snippets.await; + let snippets = snippets.await?; let language_name = language_name.as_deref(); generate_content_prompt( @@ -799,7 +779,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| theme.assistant.inline.pending_edit_background, + |theme| gpui::red(), // todo!("use the appropriate color") cx, ); } @@ -820,7 +800,7 @@ impl AssistantPanel { } fn new_conversation(&mut self, cx: &mut ViewContext) -> View { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { ConversationEditor::new( self.completion_provider.clone(), self.languages.clone(), @@ -854,8 +834,8 @@ impl AssistantPanel { self.toolbar.update(cx, |toolbar, cx| { toolbar.set_active_item(Some(&editor), cx); }); - if self.has_focus(cx) { - cx.focus(&editor); + if self.focus_handle.contains_focused(cx) { + cx.focus_view(&editor); } } else { self.toolbar.update(cx, |toolbar, cx| { @@ -891,31 +871,31 @@ impl AssistantPanel { self.completion_provider.save_credentials(cx, credential); self.api_key_editor.take(); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } } else { - cx.propagate_action(); + cx.propagate(); } } fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext) { self.completion_provider.delete_credentials(cx); self.api_key_editor = Some(build_api_key_editor(cx)); - cx.focus_self(); + self.focus_handle.focus(cx); cx.notify(); } fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext) { if self.zoomed { - cx.emit(AssistantPanelEvent::ZoomOut) + cx.emit(PanelEvent::ZoomOut) } else { - cx.emit(AssistantPanelEvent::ZoomIn) + cx.emit(PanelEvent::ZoomIn) } } fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext) { - let mut propagate_action = true; + let mut propagate = true; if let Some(search_bar) = self.toolbar.read(cx).item_of_type::() { search_bar.update(cx, |search_bar, cx| { if search_bar.show(cx) { @@ -924,12 +904,12 @@ impl AssistantPanel { search_bar.select_query(cx); cx.focus_self(); } - propagate_action = false + propagate = false } }); } - if propagate_action { - cx.propagate_action(); + if propagate { + cx.propagate(); } } @@ -942,7 +922,7 @@ impl AssistantPanel { return; } } - cx.propagate_action(); + cx.propagate(); } fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext) { @@ -976,9 +956,9 @@ impl AssistantPanel { fn render_editor_tools(&self, cx: &mut ViewContext) -> Vec { if self.active_editor().is_some() { vec![ - Self::render_split_button(cx).into_any(), - Self::render_quote_button(cx).into_any(), - Self::render_assist_button(cx).into_any(), + Self::render_split_button(cx).into_any_element(), + Self::render_quote_button(cx).into_any_element(), + Self::render_assist_button(cx).into_any_element(), ] } else { Default::default() @@ -1028,16 +1008,13 @@ impl AssistantPanel { } fn render_zoom_button(&self, cx: &mut ViewContext) -> impl IntoElement { + let zoomed = self.zoomed; IconButton::new("zoom_button", Icon::Menu) .on_click(cx.listener(|this, _event, cx| { this.toggle_zoom(&ToggleZoom, cx); })) - .tooltip(|cx| { - Tooltip::for_action( - if self.zoomed { "Zoom Out" } else { "Zoom In" }, - &ToggleZoom, - cx, - ) + .tooltip(move |cx| { + Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx) }) } @@ -1072,16 +1049,16 @@ impl AssistantPanel { cx.spawn(|this, mut cx| async move { let saved_conversation = fs.load(&path).await?; let saved_conversation = serde_json::from_str(&saved_conversation)?; - let conversation = cx.add_model(|cx| { + let conversation = cx.build_model(|cx| { Conversation::deserialize(saved_conversation, path.clone(), languages, cx) - }); + })?; this.update(&mut cx, |this, cx| { // If, by the time we've loaded the conversation, the user has already opened // the same conversation, we don't want to open it again. if let Some(ix) = this.editor_index_for_path(&path, cx) { this.set_active_editor_index(Some(ix), cx); } else { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { ConversationEditor::for_conversation(conversation, fs, workspace, cx) }); this.add_conversation(editor, cx); @@ -1120,6 +1097,7 @@ impl Render for AssistantPanel { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { if let Some(api_key_editor) = self.api_key_editor.clone() { v_stack() + .on_action(cx.listener(AssistantPanel::save_credentials)) .track_focus(&self.focus_handle) .child(Label::new( "To use the assistant panel or inline assistant, you need to add your OpenAI api key.", @@ -1159,6 +1137,15 @@ impl Render for AssistantPanel { } v_stack() + .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { + this.new_conversation(cx); + })) + .on_action(cx.listener(AssistantPanel::reset_credentials)) + .on_action(cx.listener(AssistantPanel::toggle_zoom)) + .on_action(cx.listener(AssistantPanel::deploy)) + .on_action(cx.listener(AssistantPanel::select_next_match)) + .on_action(cx.listener(AssistantPanel::select_prev_match)) + .on_action(cx.listener(AssistantPanel::handle_editor_cancel)) .track_focus(&self.focus_handle) .child(header) .children(if self.toolbar.read(cx).hidden() { @@ -1175,7 +1162,7 @@ impl Render for AssistantPanel { self.saved_conversations.len(), |this, range, cx| { range - .map(|ix| this.render_saved_conversation(ix, cx).into_any()) + .map(|ix| this.render_saved_conversation(ix, cx)) .collect() }, ) @@ -1311,17 +1298,14 @@ impl Conversation { completion_provider: Arc, ) -> Self { let markdown = language_registry.language_for_name("Markdown"); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, ""); + let buffer = cx.build_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), ""); buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -1409,8 +1393,8 @@ impl Conversation { let markdown = language_registry.language_for_name("Markdown"); let mut message_anchors = Vec::new(); let mut next_message_id = MessageId(0); - let buffer = cx.add_model(|cx| { - let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text); + let buffer = cx.build_model(|cx| { + let mut buffer = Buffer::new(0, cx.entity_id().as_u64(), saved_conversation.text); for message in saved_conversation.messages { message_anchors.push(MessageAnchor { id: message.id, @@ -1419,14 +1403,11 @@ impl Conversation { next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1)); } buffer.set_language_registry(language_registry); - cx.spawn_weak(|buffer, mut cx| async move { + cx.spawn(|buffer, mut cx| async move { let markdown = markdown.await?; - let buffer = buffer - .upgrade(&cx) - .ok_or_else(|| anyhow!("buffer was dropped"))?; buffer.update(&mut cx, |buffer: &mut Buffer, cx| { buffer.set_language(Some(markdown), cx) - }); + })?; anyhow::Ok(()) }) .detach_and_log_err(cx); @@ -1497,26 +1478,24 @@ impl Conversation { }) .collect::>(); let model = self.model.clone(); - self.pending_token_count = cx.spawn_weak(|this, mut cx| { + self.pending_token_count = cx.spawn(|this, mut cx| { async move { cx.background_executor() .timer(Duration::from_millis(200)) .await; let token_count = cx - .background() + .background_executor() .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages) }) .await?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.max_token_count = - tiktoken_rs::model::get_context_size(&this.model.full_name()); - this.token_count = Some(token_count); - cx.notify() - }); + this.update(&mut cx, |this, cx| { + this.max_token_count = + tiktoken_rs::model::get_context_size(&this.model.full_name()); + this.token_count = Some(token_count); + cx.notify() + })?; anyhow::Ok(()) } .log_err() @@ -1603,7 +1582,7 @@ impl Conversation { .unwrap(); user_messages.push(user_message); - let task = cx.spawn_weak({ + let task = cx.spawn({ |this, mut cx| async move { let assistant_message_id = assistant_message.id; let stream_completion = async { @@ -1612,59 +1591,55 @@ impl Conversation { while let Some(message) = messages.next().await { let text = message?; - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - let message_ix = this - .message_anchors + this.update(&mut cx, |this, cx| { + let message_ix = this + .message_anchors + .iter() + .position(|message| message.id == assistant_message_id)?; + this.buffer.update(cx, |buffer, cx| { + let offset = this.message_anchors[message_ix + 1..] .iter() - .position(|message| message.id == assistant_message_id)?; - this.buffer.update(cx, |buffer, cx| { - let offset = this.message_anchors[message_ix + 1..] - .iter() - .find(|message| message.start.is_valid(buffer)) - .map_or(buffer.len(), |message| { - message.start.to_offset(buffer).saturating_sub(1) - }); - buffer.edit([(offset..offset, text)], None, cx); - }); - cx.emit(ConversationEvent::StreamedCompletion); - - Some(()) + .find(|message| message.start.is_valid(buffer)) + .map_or(buffer.len(), |message| { + message.start.to_offset(buffer).saturating_sub(1) + }); + buffer.edit([(offset..offset, text)], None, cx); }); + cx.emit(ConversationEvent::StreamedCompletion); + + Some(()) + })?; smol::future::yield_now().await; } - this.upgrade(&cx) - .ok_or_else(|| anyhow!("conversation was dropped"))? - .update(&mut cx, |this, cx| { - this.pending_completions - .retain(|completion| completion.id != this.completion_count); - this.summarize(cx); - }); + this.update(&mut cx, |this, cx| { + this.pending_completions + .retain(|completion| completion.id != this.completion_count); + this.summarize(cx); + })?; anyhow::Ok(()) }; let result = stream_completion.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - if let Some(metadata) = - this.messages_metadata.get_mut(&assistant_message.id) - { - match result { - Ok(_) => { - metadata.status = MessageStatus::Done; - } - Err(error) => { - metadata.status = - MessageStatus::Error(error.to_string().trim().into()); - } + + this.update(&mut cx, |this, cx| { + if let Some(metadata) = + this.messages_metadata.get_mut(&assistant_message.id) + { + match result { + Ok(_) => { + metadata.status = MessageStatus::Done; + } + Err(error) => { + metadata.status = + MessageStatus::Error(error.to_string().trim().into()); } - cx.notify(); } - }); - } + cx.notify(); + } + }) + .ok(); } }); @@ -1999,10 +1974,10 @@ impl Conversation { None }; (path, summary) - }); + })?; if let Some(summary) = summary { - let conversation = this.read_with(&cx, |this, cx| this.serialize(cx)); + let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?; let path = if let Some(old_path) = old_path { old_path } else { @@ -2026,7 +2001,7 @@ impl Conversation { fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap()) .await?; - this.update(&mut cx, |this, _| this.path = Some(path)); + this.update(&mut cx, |this, _| this.path = Some(path))?; } Ok(()) @@ -2069,7 +2044,7 @@ impl ConversationEditor { cx: &mut ViewContext, ) -> Self { let conversation = - cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider)); + cx.build_model(|cx| Conversation::new(language_registry, cx, completion_provider)); Self::for_conversation(conversation, fs, workspace, cx) } @@ -2079,7 +2054,7 @@ impl ConversationEditor { workspace: WeakView, cx: &mut ViewContext, ) -> Self { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { 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); @@ -2093,7 +2068,7 @@ impl ConversationEditor { cx.observe(&conversation, |_, _, cx| cx.notify()), cx.subscribe(&conversation, Self::handle_conversation_event), cx.subscribe(&editor, Self::handle_editor_event), - cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)), + cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.editor)), ]; let mut this = Self { @@ -2155,7 +2130,7 @@ impl ConversationEditor { .conversation .update(cx, |conversation, _| conversation.cancel_last_assist()) { - cx.propagate_action(); + cx.propagate(); } } @@ -2247,8 +2222,8 @@ impl ConversationEditor { .anchor() .scroll_position(&snapshot.display_snapshot); - let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.); - if (scroll_position.y()..scroll_bottom).contains(&cursor_row) { + let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.); + if (scroll_position.y..scroll_bottom).contains(&cursor_row) { Some(ScrollPosition { cursor, offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y), @@ -2286,7 +2261,7 @@ impl ConversationEditor { }) .on_click({ let conversation = conversation.clone(); - move |_, _, cx| { + move |_, cx| { conversation.update(cx, |conversation, cx| { conversation.cycle_message_roles( HashSet::from_iter(Some(message_id)), @@ -2302,18 +2277,16 @@ impl ConversationEditor { .border_color(gpui::red()) .child(sender) .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) - .with_children( - if let MessageStatus::Error(error) = &message.status { - Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text(error, cx)) - .child(IconElement::new(Icon::XCircle)), - ) - } else { - None - }, - ) + .children(if let MessageStatus::Error(error) = &message.status { + Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text(error, cx)) + .child(IconElement::new(Icon::XCircle)), + ) + } else { + None + }) .into_any_element() } }), @@ -2342,36 +2315,35 @@ impl ConversationEditor { return; }; - let text = editor.read_with(cx, |editor, cx| { - let range = editor.selections.newest::(cx).range(); - let buffer = editor.buffer().read(cx).snapshot(cx); - let start_language = buffer.language_at(range.start); - let end_language = buffer.language_at(range.end); - let language_name = if start_language == end_language { - start_language.map(|language| language.name()) - } else { - None - }; - let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); + let editor = editor.read(cx); + let range = editor.selections.newest::(cx).range(); + let buffer = editor.buffer().read(cx).snapshot(cx); + let start_language = buffer.language_at(range.start); + let end_language = buffer.language_at(range.end); + let language_name = if start_language == end_language { + start_language.map(|language| language.name()) + } else { + None + }; + let language_name = language_name.as_deref().unwrap_or("").to_lowercase(); - let selected_text = buffer.text_for_range(range).collect::(); - if selected_text.is_empty() { - None + let selected_text = buffer.text_for_range(range).collect::(); + let text = if selected_text.is_empty() { + None + } else { + Some(if language_name == "markdown" { + selected_text + .lines() + .map(|line| format!("> {}", line)) + .collect::>() + .join("\n") } else { - Some(if language_name == "markdown" { - selected_text - .lines() - .map(|line| format!("> {}", line)) - .collect::>() - .join("\n") - } else { - format!("```{language_name}\n{selected_text}\n```") - }) - } - }); + format!("```{language_name}\n{selected_text}\n```") + }) + }; // Activate the panel - if !panel.read(cx).has_focus(cx) { + if !panel.focus_handle(cx).contains_focused(cx) { workspace.toggle_panel_focus::(cx); } @@ -2415,13 +2387,12 @@ impl ConversationEditor { } if spanned_messages > 1 { - cx.platform() - .write_to_clipboard(ClipboardItem::new(copied_text)); + cx.write_to_clipboard(ClipboardItem::new(copied_text)); return; } } - cx.propagate_action(); + cx.propagate(); } fn split(&mut self, _: &Split, cx: &mut ViewContext) { @@ -2492,15 +2463,30 @@ impl Render for ConversationEditor { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().relative().child(self.editor.clone()).child( - h_stack() - .absolute() - .gap_1() - .top_3() - .right_5() - .child(self.render_current_model(cx)) - .children(self.render_remaining_tokens(cx)), - ) + div() + .relative() + .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) + .capture_action(cx.listener(ConversationEditor::save)) + .capture_action(cx.listener(ConversationEditor::copy)) + .capture_action(cx.listener(ConversationEditor::cycle_message_role)) + .on_action(cx.listener(ConversationEditor::assist)) + .on_action(cx.listener(ConversationEditor::split)) + .child(self.editor.clone()) + .child( + h_stack() + .absolute() + .gap_1() + .top_3() + .right_5() + .child(self.render_current_model(cx)) + .children(self.render_remaining_tokens(cx)), + ) + } +} + +impl FocusableView for ConversationEditor { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } @@ -2577,30 +2563,40 @@ impl Render for InlineAssistant { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let measurements = self.measurements.get(); h_stack() + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::toggle_include_conversation)) + .on_action(cx.listener(Self::toggle_retrieve_context)) + .on_action(cx.listener(Self::move_up)) + .on_action(cx.listener(Self::move_down)) .child( h_stack() .justify_center() .w(measurements.gutter_width) .child( IconButton::new("include_conversation", Icon::Ai) - .action(ToggleIncludeConversation) + .action(Box::new(ToggleIncludeConversation)) .selected(self.include_conversation) - .tooltip(Tooltip::for_action( - "Include Conversation", - &ToggleIncludeConversation, - cx, - )), + .tooltip(|cx| { + Tooltip::for_action( + "Include Conversation", + &ToggleIncludeConversation, + cx, + ) + }), ) .children(if SemanticIndex::enabled(cx) { Some( IconButton::new("retrieve_context", Icon::MagnifyingGlass) - .action(ToggleRetrieveContext) + .action(Box::new(ToggleRetrieveContext)) .selected(self.retrieve_context) - .tooltip(Tooltip::for_action( - "Retrieve Context", - &ToggleRetrieveContext, - cx, - )), + .tooltip(|cx| { + Tooltip::for_action( + "Retrieve Context", + &ToggleRetrieveContext, + cx, + ) + }), ) } else { None @@ -2629,6 +2625,12 @@ impl Render for InlineAssistant { } } +impl FocusableView for InlineAssistant { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + impl InlineAssistant { fn new( id: usize, @@ -2656,10 +2658,7 @@ impl InlineAssistant { let mut subscriptions = vec![ cx.observe(&codegen, Self::handle_codegen_changed), cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events), - cx.on_focus( - &focus_handle, - cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)), - ), + cx.on_focus(&focus_handle, |this, cx| cx.focus_view(&this.prompt_editor)), ]; if let Some(semantic_index) = semantic_index.clone() { @@ -2939,42 +2938,17 @@ impl InlineAssistant { div() .id("update") .tooltip(|cx| Tooltip::text(status_text, cx)) - .child(IconElement::new(Icon::Update).color(color)) + .child(IconElement::new(Icon::Update).color(Color::Info)) .into_any_element() - Svg::new("icons/update.svg") - .with_color(theme.assistant.inline.context_status.in_progress_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.in_progress_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.in_progress_icon.container) - .with_tooltip::( - self.id, - status_text, - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), ) } SemanticIndexStatus::Indexed {} => Some( - Svg::new("icons/check.svg") - .with_color(theme.assistant.inline.context_status.complete_icon.color) - .constrained() - .with_width(theme.assistant.inline.context_status.complete_icon.width) - .contained() - .with_style(theme.assistant.inline.context_status.complete_icon.container) - .with_tooltip::( - self.id, - "Index up to date", - None, - theme.tooltip.clone(), - cx, - ) - .aligned() - .into_any(), + div() + .id("check") + .tooltip(|cx| Tooltip::text("Index up to date", cx)) + .child(IconElement::new(Icon::Check).color(Color::Success)) + .into_any_element() ), } } else { @@ -3083,7 +3057,8 @@ mod tests { let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = + cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3213,7 +3188,8 @@ mod tests { let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = + cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3310,7 +3286,8 @@ mod tests { init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); - let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider)); + let conversation = + cx.build_model(|cx| Conversation::new(registry, cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_1 = conversation.read(cx).message_anchors[0].clone(); @@ -3394,7 +3371,7 @@ mod tests { let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); let conversation = - cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); + cx.build_model(|cx| Conversation::new(registry.clone(), cx, completion_provider)); let buffer = conversation.read(cx).buffer.clone(); let message_0 = conversation.read(cx).message_anchors[0].id; let message_1 = conversation.update(cx, |conversation, cx| { @@ -3427,7 +3404,7 @@ mod tests { ] ); - let deserialized_conversation = cx.add_model(|cx| { + let deserialized_conversation = cx.build_model(|cx| { Conversation::deserialize( conversation.read(cx).serialize(cx), Default::default(), diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 529438648ab0a1a2495f60a261112ef73847d90b..50dae22eaec2f19acfe929b64aa8d3ccd31bd98a 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1961,14 +1961,14 @@ impl Editor { cx.notify(); } - // pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { - // self.cursor_shape = cursor_shape; - // cx.notify(); - // } + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { + self.cursor_shape = cursor_shape; + cx.notify(); + } - // pub fn set_collapse_matches(&mut self, collapse_matches: bool) { - // self.collapse_matches = collapse_matches; - // } + pub fn set_collapse_matches(&mut self, collapse_matches: bool) { + self.collapse_matches = collapse_matches; + } pub fn range_for_match(&self, range: &Range) -> Range { if self.collapse_matches { @@ -1977,56 +1977,47 @@ impl Editor { range.clone() } - // pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { - // if self.display_map.read(cx).clip_at_line_ends != clip { - // self.display_map - // .update(cx, |map, _| map.clip_at_line_ends = clip); - // } - // } - - // pub fn set_keymap_context_layer( - // &mut self, - // context: KeymapContext, - // cx: &mut ViewContext, - // ) { - // self.keymap_context_layers - // .insert(TypeId::of::(), context); - // cx.notify(); - // } + pub fn set_clip_at_line_ends(&mut self, clip: bool, cx: &mut ViewContext) { + if self.display_map.read(cx).clip_at_line_ends != clip { + self.display_map + .update(cx, |map, _| map.clip_at_line_ends = clip); + } + } - // pub fn remove_keymap_context_layer(&mut self, cx: &mut ViewContext) { - // self.keymap_context_layers.remove(&TypeId::of::()); - // cx.notify(); - // } + pub fn set_keymap_context_layer( + &mut self, + context: KeyContext, + cx: &mut ViewContext, + ) { + self.keymap_context_layers + .insert(TypeId::of::(), context); + cx.notify(); + } - // pub fn set_input_enabled(&mut self, input_enabled: bool) { - // self.input_enabled = input_enabled; - // } + pub fn remove_keymap_context_layer(&mut self, cx: &mut ViewContext) { + self.keymap_context_layers.remove(&TypeId::of::()); + cx.notify(); + } - // pub fn set_autoindent(&mut self, autoindent: bool) { - // if autoindent { - // self.autoindent_mode = Some(AutoindentMode::EachLine); - // } else { - // self.autoindent_mode = None; - // } - // } + pub fn set_input_enabled(&mut self, input_enabled: bool) { + self.input_enabled = input_enabled; + } - // pub fn read_only(&self) -> bool { - // self.read_only - // } + pub fn set_autoindent(&mut self, autoindent: bool) { + if autoindent { + self.autoindent_mode = Some(AutoindentMode::EachLine); + } else { + self.autoindent_mode = None; + } + } - // pub fn set_read_only(&mut self, read_only: bool) { - // self.read_only = read_only; - // } + pub fn read_only(&self) -> bool { + self.read_only + } - // pub fn set_field_editor_style( - // &mut self, - // style: Option>, - // cx: &mut ViewContext, - // ) { - // self.get_field_editor_theme = style; - // cx.notify(); - // } + pub fn set_read_only(&mut self, read_only: bool) { + self.read_only = read_only; + } fn selections_did_change( &mut self, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..f68046b2508e576dfa6d816068a3cc0b5f66b4fc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId { ElementId::NamedInteger(name.into(), id.as_u64() as usize) } } + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} diff --git a/crates/util/src/arc_cow.rs b/crates/util/src/arc_cow.rs index 86b998ff066b8dad6367921f2fed59a6d75a6e19..c6afabbbaa80046ea391e17ca85b522871f8c80c 100644 --- a/crates/util/src/arc_cow.rs +++ b/crates/util/src/arc_cow.rs @@ -44,12 +44,18 @@ impl<'a, T: ?Sized> From<&'a T> for ArcCow<'a, T> { } } -impl From> for ArcCow<'_, T> { +impl From> for ArcCow<'_, T> { fn from(s: Arc) -> Self { Self::Owned(s) } } +impl From<&'_ Arc> for ArcCow<'_, T> { + fn from(s: &'_ Arc) -> Self { + Self::Owned(s.clone()) + } +} + impl From for ArcCow<'_, str> { fn from(value: String) -> Self { Self::Owned(value.into()) From 38d41acf9bfd76274ea93679f906e9e4fc320ea9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:29:19 -0800 Subject: [PATCH 24/90] Fix rendering of shared screens in collab panel --- crates/collab_ui2/src/collab_panel.rs | 76 +++++---------------------- crates/gpui2/src/window.rs | 6 +++ 2 files changed, 19 insertions(+), 63 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 1de95f64b74de48b61b6a7ad0d01985afa45529b..bdddc8288af9dfd6780a72a7893e9bf371d65200 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1294,70 +1294,20 @@ impl CollabPanel { is_last: bool, cx: &mut ViewContext, ) -> impl IntoElement { - // enum OpenSharedScreen {} + let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize); - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - // let tree_branch = theme.tree_branch; - - // let handler = MouseEventHandler::new::( - // peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, - // cx, - // |mouse_state, cx| { - // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); - // let row = theme - // .project_row - // .in_state(is_selected) - // .style_for(mouse_state); - - // Flex::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // is_last, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/desktop.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("Screen", row.name.text.clone()) - // .aligned() - // .left() - // .contained() - // .with_style(row.name.container) - // .flex(1., false), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(row.container) - // }, - // ); - // if peer_id.is_none() { - // return handler.into_any(); - // } - // handler - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, this, cx| { - // if let Some(workspace) = this.workspace.upgrade(cx) { - // workspace.update(cx, |workspace, cx| { - // workspace.open_shared_screen(peer_id.unwrap(), cx) - // }); - // } - // }) - // .into_any() - - div() + ListItem::new(("screen", id)) + .left_child(render_tree_branch(is_last, cx)) + .child(IconButton::new(0, Icon::Screen)) + .child(Label::new("Screen")) + .when_some(peer_id, |this, _| { + this.on_click(cx.listener(move |this, _, cx| { + this.workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(peer_id.unwrap(), cx) + }); + })) + .tooltip(move |cx| Tooltip::text(format!("Open shared screen"), cx)) + }) } fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..f68046b2508e576dfa6d816068a3cc0b5f66b4fc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2816,3 +2816,9 @@ impl From<(&'static str, EntityId)> for ElementId { ElementId::NamedInteger(name.into(), id.as_u64() as usize) } } + +impl From<(&'static str, usize)> for ElementId { + fn from((name, id): (&'static str, usize)) -> Self { + ElementId::NamedInteger(name.into(), id) + } +} From 7b4b068230cee43d4dbdfb40aed89b5776e887b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 10:40:30 -0800 Subject: [PATCH 25/90] Render chat and notes buttons below the current channel --- crates/collab_ui2/src/collab_panel.rs | 111 ++++---------------------- 1 file changed, 16 insertions(+), 95 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index bdddc8288af9dfd6780a72a7893e9bf371d65200..4ce04b131be561054f5d6cc5db83dc4c1710feb0 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1360,54 +1360,14 @@ impl CollabPanel { channel_id: ChannelId, cx: &mut ViewContext, ) -> impl IntoElement { - // enum ChannelNotes {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // false, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/file.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("notes", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(*theme.channel_row.style_for(is_selected, state)) - // .with_padding_left(theme.channel_row.default_style().padding.left) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - - div() + ListItem::new("channel-notes") + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx); + })) + .left_child(render_tree_branch(false, cx)) + .child(IconButton::new(0, Icon::File)) + .child(Label::new("notes")) + .tooltip(move |cx| Tooltip::text("Open Channel Notes", cx)) } fn render_channel_chat( @@ -1415,53 +1375,14 @@ impl CollabPanel { channel_id: ChannelId, cx: &mut ViewContext, ) -> impl IntoElement { - // enum ChannelChat {} - // let host_avatar_width = theme - // .contact_avatar - // .width - // .or(theme.contact_avatar.height) - // .unwrap_or(0.); - - // MouseEventHandler::new::(ix as usize, cx, |state, cx| { - // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); - // let row = theme.project_row.in_state(is_selected).style_for(state); - - // Flex::::row() - // .with_child(render_tree_branch( - // tree_branch, - // &row.name.text, - // true, - // vec2f(host_avatar_width, theme.row_height), - // cx.font_cache(), - // )) - // .with_child( - // Svg::new("icons/conversations.svg") - // .with_color(theme.channel_hash.color) - // .constrained() - // .with_width(theme.channel_hash.width) - // .aligned() - // .left(), - // ) - // .with_child( - // Label::new("chat", theme.channel_name.text.clone()) - // .contained() - // .with_style(theme.channel_name.container) - // .aligned() - // .left() - // .flex(1., true), - // ) - // .constrained() - // .with_height(theme.row_height) - // .contained() - // .with_style(*theme.channel_row.style_for(is_selected, state)) - // .with_padding_left(theme.channel_row.default_style().padding.left) - // }) - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .into_any() - div() + ListItem::new("channel-chat") + .on_click(cx.listener(move |this, _, cx| { + this.join_channel_chat(channel_id, cx); + })) + .left_child(render_tree_branch(true, cx)) + .child(IconButton::new(0, Icon::MessageBubbles)) + .child(Label::new("chat")) + .tooltip(move |cx| Tooltip::text("Open Chat", cx)) } // fn render_channel_invite( From 27703a327912b7cd2faedb21e3606ad47567f281 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 14:04:12 -0500 Subject: [PATCH 26/90] Update tabs rendering, fix tab spacing bug Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/editor2/src/items.rs | 43 +++--- crates/ui2/src/components/indicator.rs | 3 +- crates/workspace2/src/pane.rs | 178 ++++++++++++------------- crates/workspace2/src/toolbar.rs | 6 +- 4 files changed, 114 insertions(+), 116 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 93bb37c6222932f395d738e4a8bac9ec20d7076c..1d1740bb1bc729dd84b90077c1982f3349ea8a28 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -32,7 +32,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Color, Label}; +use ui::{h_stack, Color, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, @@ -586,28 +586,25 @@ impl Item for Editor { fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement { let theme = cx.theme(); - AnyElement::new( - div() - .flex() - .flex_row() - .items_center() - .gap_2() - .child(Label::new(self.title(cx).to_string())) - .children(detail.and_then(|detail| { - let path = path_for_buffer(&self.buffer, detail, false, cx)?; - let description = path.to_string_lossy(); - - Some( - div().child( - Label::new(util::truncate_and_trailoff( - &description, - MAX_TAB_TITLE_LEN, - )) - .color(Color::Muted), - ), - ) - })), - ) + let description = detail.and_then(|detail| { + let path = path_for_buffer(&self.buffer, detail, false, cx)?; + let description = path.to_string_lossy(); + let description = description.trim(); + + if description.is_empty() { + return None; + } + + Some(util::truncate_and_trailoff(&description, MAX_TAB_TITLE_LEN)) + }); + + h_stack() + .gap_2() + .child(Label::new(self.title(cx).to_string())) + .when_some(description, |this, description| { + this.child(Label::new(description).color(Color::Muted)) + }) + .into_any_element() } fn for_each_project_item( diff --git a/crates/ui2/src/components/indicator.rs b/crates/ui2/src/components/indicator.rs index af62f9d989c12f1420afe5d054a7305cfd6e3a2b..4a94650dfc6cdd4a17290413587c2fd17898e20f 100644 --- a/crates/ui2/src/components/indicator.rs +++ b/crates/ui2/src/components/indicator.rs @@ -1,4 +1,4 @@ -use gpui::{AnyView, Div, Position}; +use gpui::{Div, Position}; use crate::prelude::*; @@ -49,6 +49,7 @@ impl RenderOnce for Indicator { fn render(self, cx: &mut WindowContext) -> Self::Rendered { div() + .flex_none() .map(|this| match self.style { IndicatorStyle::Dot => this.w_1p5().h_1p5().rounded_full(), IndicatorStyle::Bar => this.w_full().h_1p5().rounded_t_md(), diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 599c1d88c78d48dcb31a5784ab44afa3d74015c9..a2eb3d41ac28f0852a2c2d4b8872a7982b237852 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1,5 +1,5 @@ use crate::{ - item::{Item, ItemHandle, ItemSettings, WeakItemHandle}, + item::{ClosePosition, Item, ItemHandle, ItemSettings, WeakItemHandle}, toolbar::Toolbar, workspace_settings::{AutosaveSetting, WorkspaceSettings}, NewCenterTerminal, NewFile, NewSearch, SplitDirection, ToggleZoom, Workspace, @@ -27,8 +27,8 @@ use std::{ }; use ui::{ - h_stack, prelude::*, right_click_menu, Color, Icon, IconButton, IconElement, IconSize, Label, - Tooltip, + h_stack, prelude::*, right_click_menu, ButtonSize, Color, Icon, IconButton, IconSize, + Indicator, Label, Tooltip, }; use ui::{v_stack, ContextMenu}; use util::truncate_and_remove_front; @@ -1416,39 +1416,7 @@ impl Pane { cx: &mut ViewContext<'_, Pane>, ) -> impl IntoElement { let label = item.tab_content(Some(detail), cx); - let close_right = ItemSettings::get_global(cx).close_position.right(); - - let close_icon = || { - let id = item.item_id(); - - h_stack() - .id(ix) - .justify_center() - .w_4() - .h_4() - .rounded_md() - .absolute() - .map(|this| { - if close_right { - this.right_1() - } else { - this.left_1() - } - }) - .invisible() - .group_hover("", |style| style.visible()) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .on_click(cx.listener(move |pane, _, cx| { - pane.close_item_by_id(id, SaveIntent::Close, cx) - .detach_and_log_err(cx); - })) - .child( - IconElement::new(Icon::Close) - .color(Color::Muted) - .size(IconSize::XSmall), - ) - }; + let close_side = &ItemSettings::get_global(cx).close_position; let (text_color, tab_bg, tab_hover_bg, tab_active_bg) = match ix == self.active_item_index { false => ( @@ -1467,82 +1435,114 @@ impl Pane { let is_active = ix == self.active_item_index; - let tab = h_stack() - .group("") - .id(ix) - .relative() - .cursor_pointer() - .when_some(item.tab_tooltip_text(cx), |div, text| { - div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) - }) - .on_click(cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx))) - // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) - // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) - // .on_drop(|_view, state: View, cx| { - // eprintln!("{:?}", state.read(cx)); - // }) - .flex() - .items_center() - .justify_center() - .px_5() - .h(rems(1.875)) - .bg(tab_bg) + let indicator = { + let indicator_color = match (item.has_conflict(cx), item.is_dirty(cx)) { + (true, _) => Some(Color::Warning), + (_, true) => Some(Color::Accent), + (false, false) => None, + }; + + h_stack() + .w_3() + .h_3() + .justify_center() + .absolute() + .bg(gpui::red()) + .map(|this| match close_side { + ClosePosition::Left => this.right_1(), + ClosePosition::Right => this.left_1(), + }) + .when_some(indicator_color, |this, indicator_color| { + this.child(Indicator::dot().color(indicator_color)) + }) + }; + + let close_button = { + let id = item.item_id(); + + h_stack() + .invisible() + .w_3() + .h_3() + .justify_center() + .absolute() + .map(|this| match close_side { + ClosePosition::Left => this.left_1(), + ClosePosition::Right => this.right_1(), + }) + .group_hover("", |style| style.visible()) + .child( + // TODO: Fix button size + IconButton::new("close tab", Icon::Close) + .icon_color(Color::Muted) + .size(ButtonSize::None) + .icon_size(IconSize::XSmall) + .on_click(cx.listener(move |pane, _, cx| { + pane.close_item_by_id(id, SaveIntent::Close, cx) + .detach_and_log_err(cx); + })), + ) + }; + + let tab = div() .border_color(cx.theme().colors().border) - .text_color(if is_active { - cx.theme().colors().text - } else { - cx.theme().colors().text_muted - }) + .bg(tab_bg) + // 30px @ 16px/rem + .h(rems(1.875)) .map(|this| { let is_first_item = ix == 0; let is_last_item = ix == self.items.len() - 1; match ix.cmp(&self.active_item_index) { cmp::Ordering::Less => { if is_first_item { - this.ml_px().mr_px().border_b() + this.pl_px().pr_px().border_b() } else { - this.border_l().mr_px().border_b() + this.border_l().pr_px().border_b() } } cmp::Ordering::Greater => { if is_last_item { - this.mr_px().ml_px().border_b() + this.pr_px().pl_px().border_b() } else { - this.border_r().ml_px().border_b() + this.border_r().pl_px().border_b() } } cmp::Ordering::Equal => { if is_first_item { - this.ml_px().border_r().mb_px() + this.pl_px().border_r().pb_px() } else { - this.border_l().border_r().mb_px() + this.border_l().border_r().pb_px() } } } }) - // .hover(|h| h.bg(tab_hover_bg)) - // .active(|a| a.bg(tab_active_bg)) - .gap_1() - .text_color(text_color) - .children( - item.has_conflict(cx) - .then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Warning), - ) + .child( + h_stack() + .group("") + .id(ix) + .relative() + .h_full() + .cursor_pointer() + .when_some(item.tab_tooltip_text(cx), |div, text| { + div.tooltip(move |cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) }) - .or(item.is_dirty(cx).then(|| { - div().border().border_color(gpui::red()).child( - IconElement::new(Icon::ExclamationTriangle) - .size(ui::IconSize::Small) - .color(Color::Info), - ) - })), - ) - .child(label) - .child(close_icon()); + .on_click( + cx.listener(move |v: &mut Self, e, cx| v.activate_item(ix, true, true, cx)), + ) + // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) + // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) + // .on_drop(|_view, state: View, cx| { + // eprintln!("{:?}", state.read(cx)); + // }) + .px_5() + // .hover(|h| h.bg(tab_hover_bg)) + // .active(|a| a.bg(tab_active_bg)) + .gap_1() + .text_color(text_color) + .child(indicator) + .child(close_button) + .child(div().bg(gpui::green()).child(label)), + ); right_click_menu(ix).trigger(tab).menu(|cx| { ContextMenu::build(cx, |menu, cx| { diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index d47e99cb2027bfea51775e2060a4ea948342d0e7..1cc71e4d849e0d43bc94782d547e9289da180011 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -1,10 +1,10 @@ use crate::ItemHandle; use gpui::{ - div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, + AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View, ViewContext, WindowContext, }; -use ui::{h_stack, v_stack, Icon, IconButton}; -use ui::{prelude::*, Tooltip}; +use ui::prelude::*; +use ui::{h_stack, v_stack}; pub enum ToolbarItemEvent { ChangeLocation(ToolbarItemLocation), From dc7e4a4b1750d645bca1f598fec688236d8cac50 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 14:09:29 -0500 Subject: [PATCH 27/90] Remove debugging colors Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/workspace2/src/pane.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index a2eb3d41ac28f0852a2c2d4b8872a7982b237852..8c7d860a8edbf01ee19f013d1ed83d75f95ee282 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -1447,7 +1447,6 @@ impl Pane { .h_3() .justify_center() .absolute() - .bg(gpui::red()) .map(|this| match close_side { ClosePosition::Left => this.right_1(), ClosePosition::Right => this.left_1(), @@ -1541,7 +1540,7 @@ impl Pane { .text_color(text_color) .child(indicator) .child(close_button) - .child(div().bg(gpui::green()).child(label)), + .child(label), ); right_click_menu(ix).trigger(tab).menu(|cx| { @@ -1588,7 +1587,6 @@ impl Pane { .border_b() .border_r() .border_color(cx.theme().colors().border) - .bg(gpui::red()) // Nav Buttons .child( IconButton::new("navigate_backward", Icon::ArrowLeft) @@ -1615,7 +1613,6 @@ impl Pane { .flex_1() .h_full() .overflow_hidden_x() - .bg(gpui::green()) .child( div() .absolute() @@ -1639,7 +1636,6 @@ impl Pane { // Right Side .child( h_stack() - .bg(gpui::blue()) .flex() .flex_none() .gap_1() From 4c4b235b137d3c52086d31e356485e144cf892a8 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 5 Dec 2023 14:09:42 -0500 Subject: [PATCH 28/90] make ci happy Co-Authored-By: Marshall Bowers <1486634+maxdeviant@users.noreply.github.com> --- crates/quick_action_bar2/src/quick_action_bar.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 6b8f15d4c95f361ab2e881ab32174b474f16713a..3232de08adea814fcf33e7fc54b598a93bf18c25 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -5,7 +5,7 @@ use gpui::{ Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, Styled, Subscription, View, ViewContext, WeakView, }; -use search::{buffer_search, BufferSearchBar}; +use search::BufferSearchBar; use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; use workspace::{ item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -15,6 +15,7 @@ pub struct QuickActionBar { buffer_search_bar: View, active_item: Option>, _inlay_hints_enabled_subscription: Option, + #[allow(unused)] workspace: WeakView, } @@ -28,6 +29,7 @@ impl QuickActionBar { } } + #[allow(dead_code)] fn active_editor(&self) -> Option> { self.active_item .as_ref() @@ -172,6 +174,7 @@ impl QuickActionBarButton { } } + #[allow(dead_code)] pub fn meta(mut self, meta: Option>) -> Self { self.tooltip_meta = meta.map(|meta| meta.into()); self From 8141f4fd86ca06f95e0cb04875dfbe0fd5c7600b Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 14:17:22 -0500 Subject: [PATCH 29/90] Format code --- crates/theme2/src/default_theme.rs | 4 +++- crates/theme2/src/registry.rs | 4 ++-- crates/zed2/src/zed2.rs | 7 +++---- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/theme2/src/default_theme.rs b/crates/theme2/src/default_theme.rs index 269414b36a0747e5e1bfed677bfb69378ec2ab03..ab953b121a714f1bb7454c096b777f1b742b4827 100644 --- a/crates/theme2/src/default_theme.rs +++ b/crates/theme2/src/default_theme.rs @@ -1,8 +1,10 @@ use std::sync::Arc; use crate::{ + default_color_scales, one_themes::{one_dark, one_family}, - Theme, ThemeFamily, Appearance, ThemeStyles, SystemColors, ThemeColors, StatusColors, PlayerColors, SyntaxTheme, default_color_scales, + Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, Theme, ThemeColors, + ThemeFamily, ThemeStyles, }; fn zed_pro_daylight() -> Theme { diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 8e2a4d401fd201515baa5bfd42d4d2a506798b93..cb7814cb6fb88886cb95490544c00f0dc7f5612e 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -6,8 +6,8 @@ use gpui::{HighlightStyle, SharedString}; use refineable::Refineable; use crate::{ - one_themes::one_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, SystemColors, - Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, zed_pro_family, + one_themes::one_family, zed_pro_family, Appearance, PlayerColors, StatusColors, SyntaxTheme, + SystemColors, Theme, ThemeColors, ThemeFamily, ThemeStyles, UserTheme, UserThemeFamily, }; pub struct ThemeRegistry { diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index ec9d76449b6a911ff77deea1880669d17427af9b..8ce1d1d90e80d9a838f8fc5d3fb55edfc2e85b75 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -102,10 +102,9 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); toolbar.add_item(buffer_search_bar.clone(), cx); - let quick_action_bar = cx.build_view(|_| { - QuickActionBar::new(buffer_search_bar, workspace) - }); - toolbar.add_item(quick_action_bar, cx); + let quick_action_bar = cx + .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); + toolbar.add_item(quick_action_bar, cx); let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); From ab140ee4c271c33be31aa3b8e60892aff40c80c7 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 5 Dec 2023 12:07:17 -0800 Subject: [PATCH 30/90] Add event based drag API to GPUI, continue binding mouse handlers to terminal --- Cargo.lock | 1 + crates/gpui2/src/app.rs | 30 +- crates/gpui2/src/element.rs | 33 -- crates/gpui2/src/elements/div.rs | 73 +++- crates/gpui2/src/window.rs | 19 +- crates/terminal2/src/terminal2.rs | 9 +- crates/terminal_view2/Cargo.toml | 1 + crates/terminal_view2/src/terminal_element.rs | 354 ++++++++++-------- crates/terminal_view2/src/terminal_view.rs | 1 + 9 files changed, 296 insertions(+), 225 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 39683c9fc11b41700a5b5123287444979ae87bc9..349ec8f175c8f1e9adc2eaa5147e9b8a78708c3c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9478,6 +9478,7 @@ dependencies = [ "terminal2", "theme2", "thiserror", + "ui2", "util", "workspace2", ] diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index fec6f150f6c341f916e0173379aba63bebcc1ffd..a639660c8de91fb0a5dcd22f8f563bf7307b3786 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -185,7 +185,7 @@ pub struct AppContext { flushing_effects: bool, pending_updates: usize, pub(crate) actions: Rc, - pub(crate) active_drag: Option, + pub(crate) active_drag: Option, pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: HashMap>, pub(crate) frame_consumers: HashMap>, @@ -1172,6 +1172,34 @@ pub struct AnyDrag { pub cursor_offset: Point, } +pub enum AnyDragState { + EventListener, + AnyDrag(AnyDrag), +} + +impl AnyDragState { + pub fn any_drag(&self) -> Option<&AnyDrag> { + match self { + AnyDragState::EventListener => None, + AnyDragState::AnyDrag(any_drag) => Some(any_drag), + } + } + + pub fn entity_id(&self) -> Option { + match self { + AnyDragState::EventListener => None, + AnyDragState::AnyDrag(any_drag) => Some(any_drag.view.entity_id()), + } + } + + pub fn entity_type(&self) -> Option { + match self { + AnyDragState::EventListener => None, + AnyDragState::AnyDrag(any_drag) => Some(any_drag.view.entity_type()), + } + } +} + #[derive(Clone)] pub(crate) struct AnyTooltip { pub view: AnyView, diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index 3c8f678b89dca4e0d7968a9b94e2298140324479..226a477012a025474df2a58c70be56c10ef37fc3 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -370,39 +370,6 @@ impl DrawableElement { } } -// impl Element for DrawableElement { -// type State = ::State; - -// fn layout( -// &mut self, -// element_state: Option, -// cx: &mut WindowContext, -// ) -> (LayoutId, Self::State) { - -// } - -// fn paint( -// self, -// bounds: Bounds, -// element_state: &mut Self::State, -// cx: &mut WindowContext, -// ) { -// todo!() -// } -// } - -// impl RenderOnce for DrawableElement { -// type Element = Self; - -// fn element_id(&self) -> Option { -// self.element.as_ref()?.element_id() -// } - -// fn render_once(self) -> Self::Element { -// self -// } -// } - impl ElementObject for Option> where E: Element, diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index ce457fc6931246ee1e2e4d19a4a1639b37998395..e653710c0f18307f2b605c968294d436719c483f 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,10 +1,10 @@ use crate::{ - point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, - BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, - IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, - SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, - WindowContext, + point, px, Action, AnyDrag, AnyDragState, AnyElement, AnyTooltip, AnyView, AppContext, + BorrowAppContext, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, + FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, + Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, + Task, View, Visibility, WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -415,6 +415,19 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } + fn on_drag_event( + mut self, + listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, + ) -> Self + where + Self: Sized, + { + self.interactivity() + .drag_event_listeners + .push(Box::new(listener)); + self + } + fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self where Self: Sized, @@ -559,6 +572,8 @@ pub type KeyDownListener = Box; +pub type DragEventListener = Box; + pub type ActionListener = Box; pub fn div() -> Div { @@ -746,6 +761,7 @@ pub struct Interactivity { pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, pub drop_listeners: SmallVec<[(TypeId, Box); 2]>, pub click_listeners: SmallVec<[ClickListener; 2]>, + pub drag_event_listeners: SmallVec<[DragEventListener; 1]>, pub drag_listener: Option, pub hover_listener: Option>, pub tooltip_builder: Option, @@ -890,8 +906,11 @@ impl Interactivity { if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, &cx) { - if let Some(drag_state_type) = - cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) + if let Some(drag_state_type) = cx + .active_drag + .as_ref() + .and_then(|drag| drag.any_drag()) + .map(|drag| drag.view.entity_type()) { for (drop_state_type, listener) in &drop_listeners { if *drop_state_type == drag_state_type { @@ -899,11 +918,14 @@ impl Interactivity { .active_drag .take() .expect("checked for type drag state type above"); + let drag = drag.any_drag().expect("checked for any drag above"); listener(drag.view.clone(), cx); cx.notify(); cx.stop_propagation(); } } + } else { + cx.active_drag = None; } } }); @@ -911,12 +933,16 @@ impl Interactivity { let click_listeners = mem::take(&mut self.click_listeners); let drag_listener = mem::take(&mut self.drag_listener); + let drag_event_listeners = mem::take(&mut self.drag_event_listeners); - if !click_listeners.is_empty() || drag_listener.is_some() { + if !click_listeners.is_empty() + || drag_listener.is_some() + || !drag_event_listeners.is_empty() + { let pending_mouse_down = element_state.pending_mouse_down.clone(); let mouse_down = pending_mouse_down.borrow().clone(); if let Some(mouse_down) = mouse_down { - if let Some(drag_listener) = drag_listener { + if !drag_event_listeners.is_empty() || drag_listener.is_some() { let active_state = element_state.clicked_state.clone(); let interactive_bounds = interactive_bounds.clone(); @@ -924,17 +950,29 @@ impl Interactivity { if cx.active_drag.is_some() { if phase == DispatchPhase::Capture { cx.notify(); + } else if interactive_bounds.visibly_contains(&event.position, cx) + && (event.position - mouse_down.position).magnitude() + > DRAG_THRESHOLD + { + for listener in &drag_event_listeners { + listener(event, cx); + } } } else if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, cx) && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD { *active_state.borrow_mut() = ElementClickedState::default(); - let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(cursor_offset, cx); - cx.active_drag = Some(drag); - cx.notify(); - cx.stop_propagation(); + if let Some(drag_listener) = &drag_listener { + let cursor_offset = event.position - bounds.origin; + let drag = drag_listener(cursor_offset, cx); + cx.active_drag = Some(AnyDragState::AnyDrag(drag)); + cx.notify(); + cx.stop_propagation(); + } + for listener in &drag_event_listeners { + listener(event, cx); + } } }); } @@ -1197,7 +1235,7 @@ impl Interactivity { if let Some(drag) = cx.active_drag.take() { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if *state_type == drag.view.entity_type() + if Some(*state_type) == drag.entity_type() && group_bounds.contains_point(&mouse_position) { style.refine(&group_drag_style.style); @@ -1206,7 +1244,7 @@ impl Interactivity { } for (state_type, drag_over_style) in &self.drag_over_styles { - if *state_type == drag.view.entity_type() + if Some(*state_type) == drag.entity_type() && bounds .intersect(&cx.content_mask().bounds) .contains_point(&mouse_position) @@ -1263,6 +1301,7 @@ impl Default for Interactivity { action_listeners: SmallVec::new(), drop_listeners: SmallVec::new(), click_listeners: SmallVec::new(), + drag_event_listeners: SmallVec::new(), drag_listener: None, hover_listener: None, tooltip_builder: None, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..e83f3012a5a2172677101dabdf77f23efe919ce9 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1159,12 +1159,15 @@ impl<'a> WindowContext<'a> { }); if let Some(active_drag) = self.app.active_drag.take() { - self.with_z_index(1, |cx| { - let offset = cx.mouse_position() - active_drag.cursor_offset; - let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_drag.view.draw(offset, available_space, cx); - cx.active_drag = Some(active_drag); - }); + if let Some(active_drag) = active_drag.any_drag() { + self.with_z_index(1, |cx| { + let offset = cx.mouse_position() - active_drag.cursor_offset; + let available_space = + size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_drag.view.draw(offset, available_space, cx); + }); + } + self.active_drag = Some(active_drag); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.with_z_index(1, |cx| { let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); @@ -1240,10 +1243,10 @@ impl<'a> WindowContext<'a> { FileDropEvent::Entered { position, files } => { self.window.mouse_position = position; if self.active_drag.is_none() { - self.active_drag = Some(AnyDrag { + self.active_drag = Some(crate::AnyDragState::AnyDrag(AnyDrag { view: self.build_view(|_| files).into(), cursor_offset: position, - }); + })); } InputEvent::MouseDown(MouseDownEvent { position, diff --git a/crates/terminal2/src/terminal2.rs b/crates/terminal2/src/terminal2.rs index 761bee26b2882e4089ad3730b14ed9de256f0c87..197b912b1b29582b5f419250da390c9c8beb1148 100644 --- a/crates/terminal2/src/terminal2.rs +++ b/crates/terminal2/src/terminal2.rs @@ -1104,7 +1104,12 @@ impl Terminal { } } - pub fn mouse_drag(&mut self, e: MouseMoveEvent, origin: Point, region: Bounds) { + pub fn mouse_drag( + &mut self, + e: &MouseMoveEvent, + origin: Point, + region: Bounds, + ) { let position = e.position - origin; self.last_mouse_position = Some(position); @@ -1130,7 +1135,7 @@ impl Terminal { } } - fn drag_line_delta(&mut self, e: MouseMoveEvent, region: Bounds) -> Option { + fn drag_line_delta(&mut self, e: &MouseMoveEvent, region: Bounds) -> Option { //TODO: Why do these need to be doubled? Probably the same problem that the IME has let top = region.origin.y + (self.last_content.size.line_height * 2.); let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.); diff --git a/crates/terminal_view2/Cargo.toml b/crates/terminal_view2/Cargo.toml index 12e2c06504fad5adc7e502c4225a28c9bf985f30..9654bed7f56f236d0dc57196e1e25a9452c507e3 100644 --- a/crates/terminal_view2/Cargo.toml +++ b/crates/terminal_view2/Cargo.toml @@ -21,6 +21,7 @@ workspace = { package = "workspace2", path = "../workspace2" } db = { package = "db2", path = "../db2" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } terminal = { package = "terminal2", path = "../terminal2" } +ui = { package = "ui2", path = "../ui2" } smallvec.workspace = true smol.workspace = true mio-extras = "2.0.6" diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 7b7c2a304140ed2d3e91325e7bb8550281157cd7..96ffbb1ffbf8c9bfab8919edd59429667cb1f3ed 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,9 +1,10 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ - black, point, px, red, relative, transparent_black, AnyElement, Bounds, Element, ElementId, - Font, FontStyle, FontWeight, HighlightStyle, Hsla, IntoElement, LayoutId, Pixels, Point, Rgba, - ShapedLine, Style, TextRun, TextStyle, TextSystem, UnderlineStyle, ViewContext, WeakModel, - WhiteSpace, WindowContext, + black, div, point, px, red, relative, transparent_black, AnyElement, AvailableSpace, Bounds, + Element, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla, + InteractiveElement, InteractiveElementState, IntoElement, LayoutId, ModelContext, Pixels, + Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled, TextRun, TextStyle, + TextSystem, UnderlineStyle, WeakModel, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -20,12 +21,11 @@ use terminal::{ IndexedCell, Terminal, TerminalContent, TerminalSize, }; use theme::{ActiveTheme, Theme, ThemeSettings}; +use ui::Tooltip; use std::mem; use std::{fmt::Debug, ops::RangeInclusive}; -use crate::TerminalView; - ///The information generated during layout that is necessary for painting pub struct LayoutState { cells: Vec, @@ -146,14 +146,25 @@ impl LayoutRect { ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalElement { terminal: WeakModel, + focus: FocusHandle, focused: bool, cursor_visible: bool, can_navigate_to_selected_word: bool, + interactivity: gpui::Interactivity, } +impl InteractiveElement for TerminalElement { + fn interactivity(&mut self) -> &mut gpui::Interactivity { + &mut self.interactivity + } +} + +impl StatefulInteractiveElement for TerminalElement {} + impl TerminalElement { pub fn new( terminal: WeakModel, + focus: FocusHandle, focused: bool, cursor_visible: bool, can_navigate_to_selected_word: bool, @@ -161,8 +172,10 @@ impl TerminalElement { TerminalElement { terminal, focused, + focus, cursor_visible, can_navigate_to_selected_word, + interactivity: Default::default(), } } @@ -365,7 +378,6 @@ impl TerminalElement { //Setup layout information // todo!(Terminal tooltips) - // let link_style = settings.theme.editor.link_definition; // let tooltip_style = settings.theme.tooltip.clone(); let buffer_font_size = settings.buffer_font_size(cx); @@ -390,6 +402,20 @@ impl TerminalElement { let settings = ThemeSettings::get_global(cx); let theme = cx.theme().clone(); + + let link_style = HighlightStyle { + color: Some(gpui::blue()), + font_weight: None, + font_style: None, + background_color: None, + underline: Some(UnderlineStyle { + thickness: px(1.0), + color: Some(gpui::red()), + wavy: false, + }), + fade_out: None, + }; + let text_style = TextStyle { font_family, font_features, @@ -439,38 +465,19 @@ impl TerminalElement { let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); - // if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { - // terminal.last_content.last_hovered_word.clone() - // } else { - None - // } + if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { + terminal.last_content.last_hovered_word.clone() + } else { + None + } }); - // let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { - // let mut tooltip = Overlay::new( - // Empty::new() - // .contained() - // .constrained() - // .with_width(dimensions.width()) - // .with_height(dimensions.height()) - // .with_tooltip::( - // hovered_word.id, - // hovered_word.word, - // None, - // tooltip_style, - // cx, - // ), - // ) - // .with_position_mode(gpui::OverlayPositionMode::Local) - // .into_any(); - - // tooltip.layout( - // SizeConstraint::new(Point::zero(), cx.window_size()), - // view_state, - // cx, - // ); - // tooltip - // }); + let hyperlink_tooltip = last_hovered_word.clone().map(|hovered_word| { + div() + .size_full() + .id("terminal-element") + .tooltip(move |cx| Tooltip::text(hovered_word.word.clone(), cx)) + }); let TerminalContent { cells, @@ -498,10 +505,9 @@ impl TerminalElement { cells, &text_style, &cx.text_system(), - // todo!(Terminal tooltips) - last_hovered_word, - // .as_ref() - // .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), + last_hovered_word + .as_ref() + .map(|last_hovered_word| (link_style, &last_hovered_word.word_match)), cx, ); @@ -577,92 +583,95 @@ impl TerminalElement { } } - // todo!() - // fn generic_button_handler( - // connection: WeakModel, - // origin: Point, - // f: impl Fn(&mut Terminal, Point, E, &mut ModelContext), - // ) -> impl Fn(E, &mut TerminalView, &mut EventContext) { - // move |event, _: &mut TerminalView, cx| { - // cx.focus_parent(); - // if let Some(conn_handle) = connection.upgrade() { - // conn_handle.update(cx, |terminal, cx| { - // f(terminal, origin, event, cx); - - // cx.notify(); - // }) - // } - // } - // } + fn generic_button_handler( + connection: WeakModel, + origin: Point, + focus_handle: FocusHandle, + f: impl Fn(&mut Terminal, Point, &E, &mut ModelContext), + ) -> impl Fn(&E, &mut WindowContext) { + move |event, cx| { + cx.focus(&focus_handle); + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + f(terminal, origin, event, cx); + + cx.notify(); + }) + } + } + } - fn attach_mouse_handlers( - &self, + fn paint_mouse_listeners( + self, origin: Point, - visible_bounds: Bounds, mode: TermMode, - cx: &mut ViewContext, - ) { - // todo!() - // let connection = self.terminal; - - // let mut region = MouseRegion::new::(cx.view_id(), 0, visible_bounds); - - // // Terminal Emulator controlled behavior: - // region = region - // // Start selections - // .on_down(MouseButton::Left, move |event, v: &mut TerminalView, cx| { - // let terminal_view = cx.handle(); - // cx.focus(&terminal_view); - // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); - // if let Some(conn_handle) = connection.upgrade() { - // conn_handle.update(cx, |terminal, cx| { - // terminal.mouse_down(&event, origin); - - // cx.notify(); - // }) - // } - // }) - // // Update drag selections - // .on_drag(MouseButton::Left, move |event, _: &mut TerminalView, cx| { - // if event.end { - // return; - // } + bounds: Bounds, + cx: &mut WindowContext, + ) -> Self { + let focus = self.focus.clone(); + let connection = self.terminal.clone(); + + self.on_mouse_down(gpui::MouseButton::Left, { + let connection = connection.clone(); + let focus = focus.clone(); + move |e, cx| { + cx.focus(&focus); + //todo!(context menu) + // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_down(&e, origin); + + cx.notify(); + }) + } + } + }) + .on_drag_event({ + let connection = connection.clone(); + let focus = focus.clone(); + move |e, cx| { + if focus.is_focused(cx) { + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + }) + } + } + } + }) + .on_mouse_up( + gpui::MouseButton::Left, + TerminalElement::generic_button_handler( + connection.clone(), + origin, + focus.clone(), + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ) + .on_click({ + let connection = connection.clone(); + move |e, cx| { + if e.down.button == gpui::MouseButton::Right { + let mouse_mode = if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, _cx| { + terminal.mouse_mode(e.down.modifiers.shift) + }) + } else { + // If we can't get the model handle, probably can't deploy the context menu + true + }; + if !mouse_mode { + //todo!(context menu) + // view.deploy_context_menu(e.position, cx); + } + } + } + }) - // if cx.is_self_focused() { - // if let Some(conn_handle) = connection.upgrade() { - // conn_handle.update(cx, |terminal, cx| { - // terminal.mouse_drag(event, origin); - // cx.notify(); - // }) - // } - // } - // }) - // // Copy on up behavior - // .on_up( - // MouseButton::Left, - // TerminalElement::generic_button_handler( - // connection, - // origin, - // move |terminal, origin, e, cx| { - // terminal.mouse_up(&e, origin, cx); - // }, - // ), - // ) - // // Context menu - // .on_click( - // MouseButton::Right, - // move |event, view: &mut TerminalView, cx| { - // let mouse_mode = if let Some(conn_handle) = connection.upgrade() { - // conn_handle.update(cx, |terminal, _cx| terminal.mouse_mode(event.shift)) - // } else { - // // If we can't get the model handle, probably can't deploy the context menu - // true - // }; - // if !mouse_mode { - // view.deploy_context_menu(event.position, cx); - // } - // }, - // ) // .on_move(move |event, _: &mut TerminalView, cx| { // if cx.is_self_focused() { // if let Some(conn_handle) = connection.upgrade() { @@ -733,71 +742,88 @@ impl TerminalElement { } impl Element for TerminalElement { - type State = (); + type State = InteractiveElementState; fn layout( &mut self, element_state: Option, cx: &mut WindowContext<'_>, ) -> (LayoutId, Self::State) { - let mut style = Style::default(); - style.size.width = relative(1.).into(); - style.size.height = relative(1.).into(); - let layout_id = cx.request_layout(&style, None); + let (layout_id, interactive_state) = + self.interactivity + .layout(element_state, cx, |mut style, cx| { + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + let layout_id = cx.request_layout(&style, None); + + layout_id + }); - (layout_id, ()) + (layout_id, interactive_state) } - fn paint(self, bounds: Bounds, _: &mut Self::State, cx: &mut WindowContext<'_>) { - let layout = self.compute_layout(bounds, cx); + fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext<'_>) { + let mut layout = self.compute_layout(bounds, cx); let theme = cx.theme(); + cx.paint_quad( bounds, Default::default(), - theme.colors().editor_background, + layout.background_color, Default::default(), Hsla::default(), ); let origin = bounds.origin + Point::new(layout.gutter, px(0.)); - for rect in &layout.rects { - rect.paint(origin, &layout, cx); - } + let this = self.paint_mouse_listeners(origin, layout.mode, bounds, cx); - cx.with_z_index(1, |cx| { - for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, &layout, origin) - { - let hr = HighlightedRange { - start_y, //Need to change this - line_height: layout.size.line_height, - lines: highlighted_range_lines, - color: color.clone(), - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.size.line_height, - }; - hr.paint(bounds, cx); + this.interactivity + .paint(bounds, bounds.size, state, cx, |_, _, cx| { + for rect in &layout.rects { + rect.paint(origin, &layout, cx); } - } - }); - cx.with_z_index(2, |cx| { - for cell in &layout.cells { - cell.paint(origin, &layout, bounds, cx); - } - }); + cx.with_z_index(1, |cx| { + for (relative_highlighted_range, color) in + layout.relative_highlighted_ranges.iter() + { + if let Some((start_y, highlighted_range_lines)) = + to_highlighted_range_lines(relative_highlighted_range, &layout, origin) + { + let hr = HighlightedRange { + start_y, //Need to change this + line_height: layout.size.line_height, + lines: highlighted_range_lines, + color: color.clone(), + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.size.line_height, + }; + hr.paint(bounds, cx); + } + } + }); - cx.with_z_index(3, |cx| { - if let Some(cursor) = &layout.cursor { - cursor.paint(origin, cx); - } - }); + cx.with_z_index(2, |cx| { + for cell in &layout.cells { + cell.paint(origin, &layout, bounds, cx); + } + }); - // if let Some(element) = &mut element_state.hyperlink_tooltip { - // element.paint(origin, visible_bounds, view_state, cx) - // } + if this.cursor_visible { + cx.with_z_index(3, |cx| { + if let Some(cursor) = &layout.cursor { + cursor.paint(origin, cx); + } + }); + } + + if let Some(element) = layout.hyperlink_tooltip.take() { + let width: AvailableSpace = bounds.size.width.into(); + let height: AvailableSpace = bounds.size.height.into(); + element.draw(origin, Size { width, height }, cx) + } + }); } // todo!() remove? diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 63ed101f500275d2b3414e3fd5dc0de25e6ed907..5b864f4a5e76420d95a9c864d4a0e1721b81e863 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -555,6 +555,7 @@ impl Render for TerminalView { .on_action(cx.listener(TerminalView::select_all)) .child(TerminalElement::new( terminal_handle, + self.focus_handle.clone(), focused, self.should_show_cursor(focused, cx), self.can_navigate_to_selected_word, From 631e264e3ccdd9716180e8cbb9479700d1923540 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 13:17:59 -0700 Subject: [PATCH 31/90] Start on app menus --- crates/gpui2/src/app.rs | 40 ++++ crates/gpui2/src/key_dispatch.rs | 20 +- crates/gpui2/src/platform.rs | 4 +- crates/gpui2/src/platform/app_menu.rs | 96 +++++++++ crates/gpui2/src/platform/mac/platform.rs | 230 +++++++++++---------- crates/gpui2/src/platform/mac/window.rs | 2 +- crates/gpui2/src/platform/test/platform.rs | 2 +- crates/gpui2/src/window.rs | 4 +- 8 files changed, 277 insertions(+), 121 deletions(-) create mode 100644 crates/gpui2/src/platform/app_menu.rs diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index fec6f150f6c341f916e0173379aba63bebcc1ffd..8d4dc371e6a3bfd560046db2f562cb9f8ebf991d 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -425,6 +425,10 @@ impl AppContext { .collect() } + pub fn active_window(&self) -> Option { + self.platform.active_window() + } + /// Opens a new window with the given option and the root view returned by the given function. /// The function is invoked with a `WindowContext`, which can be used to interact with window-specific /// functionality. @@ -1015,6 +1019,42 @@ impl AppContext { activate(); subscription } + + pub(crate) fn clear_pending_keystrokes(&mut self) { + for window in self.windows() { + window + .update(self, |_, cx| { + cx.window + .current_frame + .dispatch_tree + .clear_pending_keystrokes() + }) + .ok(); + } + } + + pub fn is_action_available(&mut self, action: &dyn Action) -> bool { + if let Some(window) = self.active_window() { + let window_action_available = window + .update(self, |_, cx| { + if let Some(focus_id) = cx.window.focus { + cx.window + .current_frame + .dispatch_tree + .is_action_available(action, focus_id) + } else { + false + } + }) + .unwrap_or(false); + if window_action_available { + return true; + } + } + + self.global_action_listeners + .contains_key(&action.as_any().type_id()) + } } impl Context for AppContext { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 4838b1a612ce65ba33c03ac25da878a752f716d3..0df052dfdf9e0f066666f56a94295d803dfab613 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -82,13 +82,13 @@ impl DispatchTree { } } - pub fn clear_keystroke_matchers(&mut self) { + pub fn clear_pending_keystrokes(&mut self) { self.keystroke_matchers.clear(); } /// Preserve keystroke matchers from previous frames to support multi-stroke /// bindings across multiple frames. - pub fn preserve_keystroke_matchers(&mut self, old_tree: &mut Self, focus_id: Option) { + pub fn preserve_pending_keystrokes(&mut self, old_tree: &mut Self, focus_id: Option) { if let Some(node_id) = focus_id.and_then(|focus_id| self.focusable_node_id(focus_id)) { let dispatch_path = self.dispatch_path(node_id); @@ -163,6 +163,22 @@ impl DispatchTree { actions } + pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool { + if let Some(node) = self.focusable_node_ids.get(&target) { + for node_id in self.dispatch_path(*node) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; + } + } + } + false + } + pub fn bindings_for_action( &self, action: &dyn Action, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 40c555301bdf00cbad7d2ef36b2c99511daa6d90..5d3a92f052eb8eafebe7e93b2d47e82c586c270a 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -1,3 +1,4 @@ +mod app_menu; mod keystroke; #[cfg(target_os = "macos")] mod mac; @@ -32,6 +33,7 @@ use std::{ }; use uuid::Uuid; +pub use app_menu::*; pub use keystroke::*; #[cfg(target_os = "macos")] pub use mac::*; @@ -59,7 +61,7 @@ pub trait Platform: 'static { fn displays(&self) -> Vec>; fn display(&self, id: DisplayId) -> Option>; - fn main_window(&self) -> Option; + fn active_window(&self) -> Option; fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs new file mode 100644 index 0000000000000000000000000000000000000000..0f784f6585f9233c953bad50dbd6d6a2bc3207f9 --- /dev/null +++ b/crates/gpui2/src/platform/app_menu.rs @@ -0,0 +1,96 @@ +use crate::{Action, AppContext, Platform}; +use util::ResultExt; + +pub struct Menu<'a> { + pub name: &'a str, + pub items: Vec>, +} + +pub enum MenuItem<'a> { + Separator, + Submenu(Menu<'a>), + Action { + name: &'a str, + action: Box, + os_action: Option, + }, +} + +impl<'a> MenuItem<'a> { + pub fn separator() -> Self { + Self::Separator + } + + pub fn submenu(menu: Menu<'a>) -> Self { + Self::Submenu(menu) + } + + pub fn action(name: &'a str, action: impl Action) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: None, + } + } + + pub fn os_action(name: &'a str, action: impl Action, os_action: OsAction) -> Self { + Self::Action { + name, + action: Box::new(action), + os_action: Some(os_action), + } + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum OsAction { + Cut, + Copy, + Paste, + SelectAll, + Undo, + Redo, +} + +pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { + platform.on_will_open_menu(Box::new({ + let cx = cx.to_async(); + move || { + cx.update(|cx| cx.clear_pending_keystrokes()).ok(); + } + })); + + platform.on_validate_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| cx.is_action_available(action)) + .unwrap_or(false) + } + })); + + platform.on_menu_command(Box::new({ + let cx = cx.to_async(); + move |action| { + cx.update(|cx| { + // 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() { + // cx.dispatch_action(Some(view_id), action); + // true + // } else { + // false + // } + // }) + // .unwrap_or(false); + + // if dispatched { + // return; + // } + // } + // cx.dispatch_global_action_any(action); + }) + .log_err(); + } + })); +} diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 9d02c8fb938e9b6f4cd1014e5ebce4f037911bcd..6dae0afc0e59af2abaeba861bb6c22638d3b0847 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,16 +1,17 @@ use super::BoolExt; use crate::{ Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, - MacWindow, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, - Result, SemanticVersion, VideoTimestamp, WindowOptions, + ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker, + MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, NSSavePanel, NSWindow, + NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, + NSSavePanel, NSWindow, }, base::{id, nil, BOOL, YES}, foundation::{ @@ -237,114 +238,115 @@ impl MacPlatform { // application_menu // } - // unsafe fn create_menu_item( - // &self, - // item: MenuItem, - // delegate: id, - // actions: &mut Vec>, - // keystroke_matcher: &KeymapMatcher, - // ) -> id { - // match item { - // MenuItem::Separator => NSMenuItem::separatorItem(nil), - // MenuItem::Action { - // name, - // action, - // os_action, - // } => { - // // TODO - // let keystrokes = keystroke_matcher - // .bindings_for_action(action.id()) - // .find(|binding| binding.action().eq(action.as_ref())) - // .map(|binding| binding.keystrokes()); - // let selector = match os_action { - // Some(crate::OsAction::Cut) => selector("cut:"), - // Some(crate::OsAction::Copy) => selector("copy:"), - // Some(crate::OsAction::Paste) => selector("paste:"), - // Some(crate::OsAction::SelectAll) => selector("selectAll:"), - // Some(crate::OsAction::Undo) => selector("undo:"), - // Some(crate::OsAction::Redo) => selector("redo:"), - // None => selector("handleGPUIMenuItem:"), - // }; - - // let item; - // if let Some(keystrokes) = keystrokes { - // if keystrokes.len() == 1 { - // let keystroke = &keystrokes[0]; - // let mut mask = NSEventModifierFlags::empty(); - // for (modifier, flag) in &[ - // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), - // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), - // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), - // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), - // ] { - // if *modifier { - // mask |= *flag; - // } - // } - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(key_to_native(&keystroke.key).as_ref()), - // ) - // .autorelease(); - // item.setKeyEquivalentModifierMask_(mask); - // } - // // For multi-keystroke bindings, render the keystroke as part of the title. - // else { - // use std::fmt::Write; - - // let mut name = format!("{name} ["); - // for (i, keystroke) in keystrokes.iter().enumerate() { - // if i > 0 { - // name.push(' '); - // } - // write!(&mut name, "{}", keystroke).unwrap(); - // } - // name.push(']'); - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(&name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - // } else { - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - - // let tag = actions.len() as NSInteger; - // let _: () = msg_send![item, setTag: tag]; - // actions.push(action); - // item - // } - // MenuItem::Submenu(Menu { name, items }) => { - // let item = NSMenuItem::new(nil).autorelease(); - // let submenu = NSMenu::new(nil).autorelease(); - // submenu.setDelegate_(delegate); - // for item in items { - // submenu.addItem_(self.create_menu_item( - // item, - // delegate, - // actions, - // keystroke_matcher, - // )); - // } - // item.setSubmenu_(submenu); - // item.setTitle_(ns_string(name)); - // item - // } - // } - // } + unsafe fn create_menu_item( + &self, + item: MenuItem, + delegate: id, + actions: &mut Vec>, + keystroke_matcher: &KeystrokeMatcher, + ) -> id { + todo!() + // match item { + // MenuItem::Separator => NSMenuItem::separatorItem(nil), + // MenuItem::Action { + // name, + // action, + // os_action, + // } => { + // // TODO + // let keystrokes = keystroke_matcher + // .bindings_for_action(action.id()) + // .find(|binding| binding.action().eq(action.as_ref())) + // .map(|binding| binding.keystrokes()); + // let selector = match os_action { + // Some(crate::OsAction::Cut) => selector("cut:"), + // Some(crate::OsAction::Copy) => selector("copy:"), + // Some(crate::OsAction::Paste) => selector("paste:"), + // Some(crate::OsAction::SelectAll) => selector("selectAll:"), + // Some(crate::OsAction::Undo) => selector("undo:"), + // Some(crate::OsAction::Redo) => selector("redo:"), + // None => selector("handleGPUIMenuItem:"), + // }; + + // let item; + // if let Some(keystrokes) = keystrokes { + // if keystrokes.len() == 1 { + // let keystroke = &keystrokes[0]; + // let mut mask = NSEventModifierFlags::empty(); + // for (modifier, flag) in &[ + // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), + // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), + // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), + // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), + // ] { + // if *modifier { + // mask |= *flag; + // } + // } + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // ns_string(key_to_native(&keystroke.key).as_ref()), + // ) + // .autorelease(); + // item.setKeyEquivalentModifierMask_(mask); + // } + // // For multi-keystroke bindings, render the keystroke as part of the title. + // else { + // use std::fmt::Write; + + // let mut name = format!("{name} ["); + // for (i, keystroke) in keystrokes.iter().enumerate() { + // if i > 0 { + // name.push(' '); + // } + // write!(&mut name, "{}", keystroke).unwrap(); + // } + // name.push(']'); + + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(&name), + // selector, + // ns_string(""), + // ) + // .autorelease(); + // } + // } else { + // item = NSMenuItem::alloc(nil) + // .initWithTitle_action_keyEquivalent_( + // ns_string(name), + // selector, + // ns_string(""), + // ) + // .autorelease(); + // } + + // let tag = actions.len() as NSInteger; + // let _: () = msg_send![item, setTag: tag]; + // actions.push(action); + // item + // } + // MenuItem::Submenu(Menu { name, items }) => { + // let item = NSMenuItem::new(nil).autorelease(); + // let submenu = NSMenu::new(nil).autorelease(); + // submenu.setDelegate_(delegate); + // for item in items { + // submenu.addItem_(self.create_menu_item( + // item, + // delegate, + // actions, + // keystroke_matcher, + // )); + // } + // item.setSubmenu_(submenu); + // item.setTitle_(ns_string(name)); + // item + // } + // } + } } impl Platform for MacPlatform { @@ -479,8 +481,8 @@ impl Platform for MacPlatform { MacDisplay::find_by_id(id).map(|screen| Rc::new(screen) as Rc<_>) } - fn main_window(&self) -> Option { - MacWindow::main_window() + fn active_window(&self) -> Option { + MacWindow::active_window() } fn open_window( diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index 5b72c10851ff555b08669d8db96e143509e8ad46..ba9a67e1580c16dfdbfc0849d8d1678af308c96a 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -662,7 +662,7 @@ impl MacWindow { } } - pub fn main_window() -> Option { + pub fn active_window() -> Option { unsafe { let app = NSApplication::sharedApplication(nil); let main_window: id = msg_send![app, mainWindow]; diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 6fa706f617119bfad728078a378309c16ab19df4..264273730510365d319120e3faaa0efd35076a1d 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -127,7 +127,7 @@ impl Platform for TestPlatform { self.displays().iter().find(|d| d.id() == id).cloned() } - fn main_window(&self) -> Option { + fn active_window(&self) -> Option { unimplemented!() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8eb14769bf1e45e30a468e9f32d694201591be86..8645554e5af83600b6ff6438869048fee09b46d1 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -430,7 +430,7 @@ impl<'a> WindowContext<'a> { self.window .current_frame .dispatch_tree - .clear_keystroke_matchers(); + .clear_pending_keystrokes(); self.app.push_effect(Effect::FocusChanged { window_handle: self.window.handle, focused: Some(focus_id), @@ -1177,7 +1177,7 @@ impl<'a> WindowContext<'a> { self.window .current_frame .dispatch_tree - .preserve_keystroke_matchers( + .preserve_pending_keystrokes( &mut self.window.previous_frame.dispatch_tree, self.window.focus, ); From 863222edc5524e864cae584d918854bb4708d217 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 12:57:23 -0800 Subject: [PATCH 32/90] Get following working Restore a single event type on Item trait, so that the workspace can subscribe to it and handle following events. --- crates/diagnostics2/src/diagnostics.rs | 47 +++----------- crates/editor2/src/editor.rs | 27 ++------ crates/editor2/src/editor_tests.rs | 4 +- crates/editor2/src/items.rs | 75 +++++++++++++++------- crates/terminal_view2/src/terminal_view.rs | 6 ++ crates/welcome2/src/welcome.rs | 6 ++ crates/workspace2/src/item.rs | 48 +++++++------- crates/workspace2/src/shared_screen.rs | 10 ++- crates/workspace2/src/workspace2.rs | 4 -- 9 files changed, 117 insertions(+), 110 deletions(-) diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index dd01f90b9f0623b3658673304464229411e3b801..44acc285e8231a48e20c791f38f09f6619e99d08 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -88,7 +88,7 @@ struct DiagnosticGroupState { block_count: usize, } -impl EventEmitter for ProjectDiagnosticsEditor {} +impl EventEmitter for ProjectDiagnosticsEditor {} impl Render for ProjectDiagnosticsEditor { type Element = Focusable
; @@ -158,7 +158,7 @@ impl ProjectDiagnosticsEditor { }); let editor_event_subscription = cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { - Self::emit_item_event_for_editor_event(event, cx); + cx.emit(event.clone()); if event == &EditorEvent::Focused && this.path_states.is_empty() { cx.focus(&this.focus_handle); } @@ -183,40 +183,6 @@ impl ProjectDiagnosticsEditor { this } - fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext) { - match event { - EditorEvent::Closed => cx.emit(ItemEvent::CloseItem), - - EditorEvent::Saved | EditorEvent::TitleChanged => { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::Reparsed => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::SelectionsChanged { local } if *local => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::DirtyChanged => { - cx.emit(ItemEvent::UpdateTab); - } - - EditorEvent::BufferEdited => { - cx.emit(ItemEvent::Edit); - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - - EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { - cx.emit(ItemEvent::Edit); - } - - _ => {} - } - } - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { if let Some(existing) = workspace.item_of_type::(cx) { workspace.activate_item(&existing, cx); @@ -333,8 +299,7 @@ impl ProjectDiagnosticsEditor { this.update(&mut cx, |this, cx| { this.summary = this.project.read(cx).diagnostic_summary(false, cx); - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(EditorEvent::TitleChanged); })?; anyhow::Ok(()) } @@ -649,6 +614,12 @@ impl FocusableView for ProjectDiagnosticsEditor { } impl Item for ProjectDiagnosticsEditor { + type Event = EditorEvent; + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } + fn deactivated(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |editor, cx| editor.deactivated(cx)); } diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 529438648ab0a1a2495f60a261112ef73847d90b..a77e1dcc3b9621080e4e78ef94f80fff2c18112e 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1675,8 +1675,7 @@ impl Editor { if let Some(project) = project.as_ref() { if buffer.read(cx).is_singleton() { project_subscriptions.push(cx.observe(project, |_, _, cx| { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + cx.emit(EditorEvent::TitleChanged); })); } project_subscriptions.push(cx.subscribe(project, |editor, _, event, cx| { @@ -2141,10 +2140,6 @@ impl Editor { cx.emit(SearchEvent::ActiveMatchChanged) } - if local { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - cx.notify(); } @@ -8573,8 +8568,6 @@ impl Editor { self.update_visible_copilot_suggestion(cx); } cx.emit(EditorEvent::BufferEdited); - cx.emit(ItemEvent::Edit); - cx.emit(ItemEvent::UpdateBreadcrumbs); cx.emit(SearchEvent::MatchesInvalidated); if *sigleton_buffer_edited { @@ -8622,20 +8615,14 @@ impl Editor { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() }) } - multi_buffer::Event::Reparsed => { - cx.emit(ItemEvent::UpdateBreadcrumbs); - } - multi_buffer::Event::DirtyChanged => { - cx.emit(ItemEvent::UpdateTab); - } - multi_buffer::Event::Saved - | multi_buffer::Event::FileHandleChanged - | multi_buffer::Event::Reloaded => { - cx.emit(ItemEvent::UpdateTab); - cx.emit(ItemEvent::UpdateBreadcrumbs); + multi_buffer::Event::Reparsed => cx.emit(EditorEvent::Reparsed), + multi_buffer::Event::DirtyChanged => cx.emit(EditorEvent::DirtyChanged), + multi_buffer::Event::Saved => cx.emit(EditorEvent::Saved), + multi_buffer::Event::FileHandleChanged | multi_buffer::Event::Reloaded => { + cx.emit(EditorEvent::TitleChanged) } multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged), - multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem), + multi_buffer::Event::Closed => cx.emit(EditorEvent::Closed), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); } diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 424da8987eb6d673f0e789d4b8ae8b1620967045..571cbd84bb179be0b1562dd07f5c7a0114e1b8e4 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -32,7 +32,7 @@ use util::{ test::{marked_text_ranges, marked_text_ranges_by, sample_text, TextRangeMarker}, }; use workspace::{ - item::{FollowEvent, FollowableEvents, FollowableItem, Item, ItemHandle}, + item::{FollowEvent, FollowableItem, Item, ItemHandle}, NavigationEntry, ViewId, }; @@ -6478,7 +6478,7 @@ async fn test_following(cx: &mut gpui::TestAppContext) { cx.subscribe( &follower.root_view(cx).unwrap(), move |_, _, event: &EditorEvent, cx| { - if matches!(event.to_follow_event(), Some(FollowEvent::Unfollow)) { + if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) { *is_still_following.borrow_mut() = false; } diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 93bb37c6222932f395d738e4a8bac9ec20d7076c..b5eb99a32da740bd16cc89ff3c45ca4527f52d45 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -35,7 +35,7 @@ use theme::{ActiveTheme, Theme}; use ui::{Color, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ - item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}, + item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, StatusItemView, }; use workspace::{ @@ -46,27 +46,7 @@ use workspace::{ pub const MAX_TAB_TITLE_LEN: usize = 24; -impl FollowableEvents for EditorEvent { - fn to_follow_event(&self) -> Option { - match self { - EditorEvent::Edited => Some(FollowEvent::Unfollow), - EditorEvent::SelectionsChanged { local } - | EditorEvent::ScrollPositionChanged { local, .. } => { - if *local { - Some(FollowEvent::Unfollow) - } else { - None - } - } - _ => None, - } - } -} - -impl EventEmitter for Editor {} - impl FollowableItem for Editor { - type FollowableEvent = EditorEvent; fn remote_id(&self) -> Option { self.remote_id } @@ -241,9 +221,24 @@ impl FollowableItem for Editor { })) } + fn to_follow_event(event: &EditorEvent) -> Option { + match event { + EditorEvent::Edited => Some(FollowEvent::Unfollow), + EditorEvent::SelectionsChanged { local } + | EditorEvent::ScrollPositionChanged { local, .. } => { + if *local { + Some(FollowEvent::Unfollow) + } else { + None + } + } + _ => None, + } + } + fn add_event_to_update_proto( &self, - event: &Self::FollowableEvent, + event: &EditorEvent, update: &mut Option, cx: &WindowContext, ) -> bool { @@ -528,6 +523,8 @@ fn deserialize_anchor(buffer: &MultiBufferSnapshot, anchor: proto::EditorAnchor) } impl Item for Editor { + type Event = EditorEvent; + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { if let Ok(data) = data.downcast::() { let newest_selection = self.selections.newest::(cx); @@ -841,6 +838,40 @@ impl Item for Editor { Some("Editor") } + fn to_item_events(event: &EditorEvent, mut f: impl FnMut(ItemEvent)) { + match event { + EditorEvent::Closed => f(ItemEvent::CloseItem), + + EditorEvent::Saved | EditorEvent::TitleChanged => { + f(ItemEvent::UpdateTab); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::Reparsed => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::SelectionsChanged { local } if *local => { + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::DirtyChanged => { + f(ItemEvent::UpdateTab); + } + + EditorEvent::BufferEdited => { + f(ItemEvent::Edit); + f(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { + f(ItemEvent::Edit); + } + + _ => {} + } + } + fn deserialize( project: Model, _workspace: WeakView, diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index e184fa68762b3480732c222f713069b517b8412b..570b37ba098b86c159b7acce1e6941402336ec97 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -736,6 +736,8 @@ impl InputHandler for TerminalView { } impl Item for TerminalView { + type Event = ItemEvent; + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { Some(self.terminal().read(cx).title().into()) } @@ -843,6 +845,10 @@ impl Item for TerminalView { // .detach(); self.workspace_id = workspace.database_id(); } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } } impl SearchableItem for TerminalView { diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs index 441c2bf69663084e40d354eccefb8d7bbb66ce49..db348ab0a1a7115586f38ffb7acb37671c4b15a9 100644 --- a/crates/welcome2/src/welcome.rs +++ b/crates/welcome2/src/welcome.rs @@ -259,6 +259,8 @@ impl FocusableView for WelcomePage { } impl Item for WelcomePage { + type Event = ItemEvent; + fn tab_content(&self, _: Option, _: &WindowContext) -> AnyElement { "Welcome to Zed!".into_any() } @@ -278,4 +280,8 @@ impl Item for WelcomePage { _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), })) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) { + f(*event) + } } diff --git a/crates/workspace2/src/item.rs b/crates/workspace2/src/item.rs index e7cdb2f861b9f911019ed1b191653885d76b6402..536ebd980e6cc66fae6cb56d15f0bdead58fda1d 100644 --- a/crates/workspace2/src/item.rs +++ b/crates/workspace2/src/item.rs @@ -78,7 +78,7 @@ impl Settings for ItemSettings { } } -#[derive(Eq, PartialEq, Hash, Debug)] +#[derive(Clone, Copy, Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { CloseItem, UpdateTab, @@ -92,7 +92,9 @@ pub struct BreadcrumbText { pub highlights: Option, HighlightStyle)>>, } -pub trait Item: FocusableView + EventEmitter { +pub trait Item: FocusableView + EventEmitter { + type Event; + fn deactivated(&mut self, _: &mut ViewContext) {} fn workspace_deactivated(&mut self, _: &mut ViewContext) {} fn navigate(&mut self, _: Box, _: &mut ViewContext) -> bool { @@ -155,6 +157,8 @@ pub trait Item: FocusableView + EventEmitter { unimplemented!("reload() must be implemented if can_save() returns true") } + fn to_item_events(event: &Self::Event, f: impl FnMut(ItemEvent)); + fn act_as_type<'a>( &'a self, type_id: TypeId, @@ -206,12 +210,12 @@ pub trait Item: FocusableView + EventEmitter { } pub trait ItemHandle: 'static + Send { - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn tab_tooltip_text(&self, cx: &AppContext) -> Option; fn tab_description(&self, detail: usize, cx: &AppContext) -> Option; fn tab_content(&self, detail: Option, cx: &WindowContext) -> AnyElement; @@ -285,20 +289,20 @@ impl dyn ItemHandle { } impl ItemHandle for View { - fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { - self.focus_handle(cx) - } - fn subscribe_to_item_events( &self, cx: &mut WindowContext, - handler: Box, + handler: Box, ) -> gpui::Subscription { cx.subscribe(self, move |_, event, cx| { - handler(event, cx); + T::to_item_events(event, |item_event| handler(item_event, cx)); }) } + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.focus_handle(cx) + } + fn tab_tooltip_text(&self, cx: &AppContext) -> Option { self.read(cx).tab_tooltip_text(cx) } @@ -461,7 +465,7 @@ impl ItemHandle for View { } } - match event { + T::to_item_events(event, |event| match event { ItemEvent::CloseItem => { pane.update(cx, |pane, cx| { pane.close_item_by_id(item.item_id(), crate::SaveIntent::Close, cx) @@ -489,7 +493,7 @@ impl ItemHandle for View { } _ => {} - } + }); })); cx.on_blur(&self.focus_handle(cx), move |workspace, cx| { @@ -655,12 +659,7 @@ pub enum FollowEvent { Unfollow, } -pub trait FollowableEvents { - fn to_follow_event(&self) -> Option; -} - pub trait FollowableItem: Item { - type FollowableEvent: FollowableEvents; fn remote_id(&self) -> Option; fn to_state_proto(&self, cx: &WindowContext) -> Option; fn from_state_proto( @@ -670,9 +669,10 @@ pub trait FollowableItem: Item { state: &mut Option, cx: &mut WindowContext, ) -> Option>>>; + fn to_follow_event(event: &Self::Event) -> Option; fn add_event_to_update_proto( &self, - event: &Self::FollowableEvent, + event: &Self::Event, update: &mut Option, cx: &WindowContext, ) -> bool; @@ -683,7 +683,6 @@ pub trait FollowableItem: Item { cx: &mut ViewContext, ) -> Task>; fn is_project_item(&self, cx: &WindowContext) -> bool; - fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext); } @@ -739,10 +738,7 @@ impl FollowableItemHandle for View { } fn to_follow_event(&self, event: &dyn Any) -> Option { - event - .downcast_ref() - .map(T::FollowableEvent::to_follow_event) - .flatten() + T::to_follow_event(event.downcast_ref()?) } fn apply_update_proto( @@ -929,6 +925,12 @@ pub mod test { } impl Item for TestItem { + type Event = ItemEvent; + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + f(*event) + } + fn tab_description(&self, detail: usize, _: &AppContext) -> Option { self.tab_descriptions.as_ref().and_then(|descriptions| { let description = *descriptions.get(detail).or_else(|| descriptions.last())?; diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs index c4bcb31958afcaf3e69b37ea116df7baa9a91f41..134dfc66bb82a42867c7fdb9d32b4cca359a0337 100644 --- a/crates/workspace2/src/shared_screen.rs +++ b/crates/workspace2/src/shared_screen.rs @@ -59,7 +59,6 @@ impl SharedScreen { } impl EventEmitter for SharedScreen {} -impl EventEmitter for SharedScreen {} impl FocusableView for SharedScreen { fn focus_handle(&self, _: &AppContext) -> FocusHandle { @@ -79,9 +78,12 @@ impl Render for SharedScreen { } impl Item for SharedScreen { + type Event = Event; + fn tab_tooltip_text(&self, _: &AppContext) -> Option { Some(format!("{}'s screen", self.user.github_login).into()) } + fn deactivated(&mut self, cx: &mut ViewContext) { if let Some(nav_history) = self.nav_history.as_mut() { nav_history.push::<()>(None, cx); @@ -111,4 +113,10 @@ impl Item for SharedScreen { let track = self.track.upgrade()?; Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) } + + fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) { + match event { + Event::Close => f(ItemEvent::CloseItem), + } + } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index e6b259eaf65983878902428b976ee2a14220703d..3780f56b843103438e7646311435516675cfd734 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2625,8 +2625,6 @@ impl Workspace { update: proto::UpdateFollowers, cx: &mut AsyncWindowContext, ) -> Result<()> { - dbg!("process_leader_update", &update); - match update.variant.ok_or_else(|| anyhow!("invalid update"))? { proto::update_followers::Variant::UpdateActiveView(update_active_view) => { this.update(cx, |this, _| { @@ -3880,8 +3878,6 @@ impl WorkspaceStore { let leader_id = envelope.original_sender_id()?; let update = envelope.payload; - dbg!("handle_upate_followers"); - this.update(&mut cx, |this, cx| { for workspace in &this.workspaces { workspace.update(cx, |workspace, cx| { From f2faa70f736252f7377935dee2fe72f7801a40de Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 13:34:12 -0800 Subject: [PATCH 33/90] Make Window::on_next_frame work in tests --- .../gpui2/src/platform/mac/display_linker.rs | 3 +- crates/gpui2/src/platform/test/platform.rs | 29 ++++++++++++------- 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/crates/gpui2/src/platform/mac/display_linker.rs b/crates/gpui2/src/platform/mac/display_linker.rs index b63cf24e2689d1d249016d86b82a62ffd2d3946b..d8f5a675a58f4204e644f5661dfc5078b9ae4295 100644 --- a/crates/gpui2/src/platform/mac/display_linker.rs +++ b/crates/gpui2/src/platform/mac/display_linker.rs @@ -7,6 +7,7 @@ use std::{ use crate::DisplayId; use collections::HashMap; use parking_lot::Mutex; +pub use sys::CVSMPTETime as SmtpeTime; pub use sys::CVTimeStamp as VideoTimestamp; pub(crate) struct MacDisplayLinker { @@ -153,7 +154,7 @@ mod sys { kCVTimeStampTopField | kCVTimeStampBottomField; #[repr(C)] - #[derive(Clone, Copy)] + #[derive(Clone, Copy, Default)] pub struct CVSMPTETime { pub subframes: i16, pub subframe_divisor: i16, diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index fa4b6e18c587521d88d8d4a4fd4041952c584f4a..2cbc228c72b2c51761982d0eb0e70f5a7450e5b4 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -147,18 +147,25 @@ impl Platform for TestPlatform { fn set_display_link_output_callback( &self, _display_id: DisplayId, - _callback: Box, + mut callback: Box, ) { - unimplemented!() - } - - fn start_display_link(&self, _display_id: DisplayId) { - unimplemented!() - } - - fn stop_display_link(&self, _display_id: DisplayId) { - unimplemented!() - } + let timestamp = crate::VideoTimestamp { + version: 0, + video_time_scale: 0, + video_time: 0, + host_time: 0, + rate_scalar: 0.0, + video_refresh_period: 0, + smpte_time: crate::SmtpeTime::default(), + flags: 0, + reserved: 0, + }; + callback(×tamp, ×tamp) + } + + fn start_display_link(&self, _display_id: DisplayId) {} + + fn stop_display_link(&self, _display_id: DisplayId) {} fn open_url(&self, _url: &str) { unimplemented!() From 02e507b97384e97493ffe0092bf009e6a6601987 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 16:34:21 -0500 Subject: [PATCH 34/90] Update breadcrumb rendering (#3505) This PR updates the rendering of the breadcrumb. Release Notes: - N/A Co-authored-by: Nate Butler --- crates/breadcrumbs2/src/breadcrumbs.rs | 81 +++++++++++++------------- 1 file changed, 39 insertions(+), 42 deletions(-) diff --git a/crates/breadcrumbs2/src/breadcrumbs.rs b/crates/breadcrumbs2/src/breadcrumbs.rs index 75195a315930e2525ed1b01aea36f57e6d30b699..1c577fa3105ea2d5b3d1aa81aa070dce136d3d06 100644 --- a/crates/breadcrumbs2/src/breadcrumbs.rs +++ b/crates/breadcrumbs2/src/breadcrumbs.rs @@ -1,10 +1,10 @@ use gpui::{ - Component, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, + Div, Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, ViewContext, WeakView, }; use itertools::Itertools; use theme::ActiveTheme; -use ui::{ButtonCommon, ButtonLike, ButtonStyle, Clickable, Disableable, Label}; +use ui::{prelude::*, ButtonLike, ButtonStyle, Label}; use workspace::{ item::{ItemEvent, ItemHandle}, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -36,54 +36,51 @@ impl EventEmitter for Breadcrumbs {} impl EventEmitter for Breadcrumbs {} impl Render for Breadcrumbs { - type Element = Component; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let button = ButtonLike::new("breadcrumbs") - .style(ButtonStyle::Transparent) - .disabled(true); + let element = h_stack().text_ui(); + + let Some(active_item) = &self + .active_item + .as_ref() + .filter(|item| item.downcast::().is_some()) + else { + return element; + }; - let active_item = match &self.active_item { - Some(active_item) => active_item, - None => return button.into_element(), + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element; }; - let not_editor = active_item.downcast::().is_none(); - let breadcrumbs = match active_item.breadcrumbs(cx.theme(), cx) { - Some(breadcrumbs) => breadcrumbs, - None => return button.into_element(), - } - .into_iter() - .map(|breadcrumb| { - StyledText::new(breadcrumb.text) - .with_highlights(&cx.text_style(), breadcrumb.highlights.unwrap_or_default()) + let highlighted_segments = segments.into_iter().map(|segment| { + StyledText::new(segment.text) + .with_highlights(&cx.text_style(), segment.highlights.unwrap_or_default()) .into_any() }); + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").into_any_element() + }); - let button = button.children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" › ").into_any_element() - })); - - if not_editor || !self.pane_focused { - return button.into_element(); - } - - // let this = cx.view().downgrade(); - button - .style(ButtonStyle::Filled) - .disabled(false) - .on_click(move |_, _cx| { - todo!("outline::toggle"); - // this.update(cx, |this, cx| { - // if let Some(workspace) = this.workspace.upgrade() { - // workspace.update(cx, |_workspace, _cx| { - // outline::toggle(workspace, &Default::default(), cx) - // }) - // } - // }) - // .ok(); - }) - .into_element() + element.child( + ButtonLike::new("toggle outline view") + .style(ButtonStyle::Subtle) + .child(h_stack().gap_1().children(breadcrumbs)) + // We disable the button when it is not focused + // due to ... @julia what was the reason again? + .disabled(!self.pane_focused) + .on_click(move |_, _cx| { + todo!("outline::toggle"); + // this.update(cx, |this, cx| { + // if let Some(workspace) = this.workspace.upgrade() { + // workspace.update(cx, |_workspace, _cx| { + // outline::toggle(workspace, &Default::default(), cx) + // }) + // } + // }) + // .ok(); + }), + ) } } From 79567d1c87cf9cd7c46fa3cc92561dfd3911204e Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 15:49:06 -0700 Subject: [PATCH 35/90] Add AppContext::dispatch_action and use it for app menu actions Co-Authored-By: Marshall Co-Authored-By: Julia --- crates/gpui2/src/app.rs | 61 +++++++++++++++++++++- crates/gpui2/src/platform.rs | 6 +-- crates/gpui2/src/platform/app_menu.rs | 27 ++-------- crates/gpui2/src/platform/mac/platform.rs | 6 +-- crates/gpui2/src/platform/test/platform.rs | 6 +-- 5 files changed, 73 insertions(+), 33 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 8d4dc371e6a3bfd560046db2f562cb9f8ebf991d..74712feb287468ef67917235b544ffd85ecf8400 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -39,7 +39,10 @@ use std::{ sync::{atomic::Ordering::SeqCst, Arc}, time::Duration, }; -use util::http::{self, HttpClient}; +use util::{ + http::{self, HttpClient}, + ResultExt, +}; /// Temporary(?) wrapper around RefCell to help us debug any double borrows. /// Strongly consider removing after stabilization. @@ -1055,6 +1058,62 @@ impl AppContext { self.global_action_listeners .contains_key(&action.as_any().type_id()) } + + pub fn dispatch_action(&mut self, action: &dyn Action) { + self.propagate_event = true; + + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in &global_listeners { + listener(action, DispatchPhase::Capture, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + + if self.propagate_event { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .log_err(); + } + } + + if self.propagate_event { + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action, DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } + } } impl Context for AppContext { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 5d3a92f052eb8eafebe7e93b2d47e82c586c270a..96be670af7dbc92af50571ab228dcaa663f90a75 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -92,9 +92,9 @@ pub trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); - fn on_menu_command(&self, callback: Box); - fn on_will_open_menu(&self, callback: Box); - fn on_validate_menu_command(&self, callback: Box bool>); + fn on_app_menu_action(&self, callback: Box); + fn on_will_open_app_menu(&self, callback: Box); + fn on_validate_app_menu_command(&self, callback: Box bool>); fn os_name(&self) -> &'static str; fn os_version(&self) -> Result; diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs index 0f784f6585f9233c953bad50dbd6d6a2bc3207f9..bfac1366b286ce58f1c423a2da13157881e7c3c8 100644 --- a/crates/gpui2/src/platform/app_menu.rs +++ b/crates/gpui2/src/platform/app_menu.rs @@ -53,14 +53,14 @@ pub enum OsAction { } pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { - platform.on_will_open_menu(Box::new({ + platform.on_will_open_app_menu(Box::new({ let cx = cx.to_async(); move || { cx.update(|cx| cx.clear_pending_keystrokes()).ok(); } })); - platform.on_validate_menu_command(Box::new({ + platform.on_validate_app_menu_command(Box::new({ let cx = cx.to_async(); move |action| { cx.update(|cx| cx.is_action_available(action)) @@ -68,29 +68,10 @@ pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { } })); - platform.on_menu_command(Box::new({ + platform.on_app_menu_action(Box::new({ let cx = cx.to_async(); move |action| { - cx.update(|cx| { - // 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() { - // cx.dispatch_action(Some(view_id), action); - // true - // } else { - // false - // } - // }) - // .unwrap_or(false); - - // if dispatched { - // return; - // } - // } - // cx.dispatch_global_action_any(action); - }) - .log_err(); + cx.update(|cx| cx.dispatch_action(action)).log_err(); } })); } diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 6dae0afc0e59af2abaeba861bb6c22638d3b0847..d7fc37f0def0736d189a891c733ed8e09997be49 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -683,15 +683,15 @@ impl Platform for MacPlatform { } } - fn on_menu_command(&self, callback: Box) { + fn on_app_menu_action(&self, callback: Box) { self.0.lock().menu_command = Some(callback); } - fn on_will_open_menu(&self, callback: Box) { + fn on_will_open_app_menu(&self, callback: Box) { self.0.lock().will_open_menu = Some(callback); } - fn on_validate_menu_command(&self, callback: Box bool>) { + fn on_validate_app_menu_command(&self, callback: Box bool>) { self.0.lock().validate_menu_command = Some(callback); } diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 264273730510365d319120e3faaa0efd35076a1d..952a9a96cac98e85863931f3e726d71b85e58896 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -205,15 +205,15 @@ impl Platform for TestPlatform { unimplemented!() } - fn on_menu_command(&self, _callback: Box) { + fn on_app_menu_action(&self, _callback: Box) { unimplemented!() } - fn on_will_open_menu(&self, _callback: Box) { + fn on_will_open_app_menu(&self, _callback: Box) { unimplemented!() } - fn on_validate_menu_command(&self, _callback: Box bool>) { + fn on_validate_app_menu_command(&self, _callback: Box bool>) { unimplemented!() } From 82534b66125286c6355cdb40c19fd698471e8b52 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 5 Dec 2023 16:37:01 -0700 Subject: [PATCH 36/90] Get app menus basically working - Everything is still disabled when there is no active window. Co-Authored-By: Marshall --- crates/editor2/src/display_map/block_map.rs | 8 +- crates/editor2/src/display_map/wrap_map.rs | 2 +- crates/editor2/src/editor_tests.rs | 6 +- crates/gpui2/src/app.rs | 21 +- crates/gpui2/src/app/test_context.rs | 24 +- crates/gpui2/src/platform.rs | 11 +- crates/gpui2/src/platform/app_menu.rs | 2 +- crates/gpui2/src/platform/mac/platform.rs | 341 ++++++++++---------- crates/gpui2/src/platform/test/platform.rs | 9 +- crates/settings2/src/settings_file.rs | 36 --- crates/zed2/src/app_menus.rs | 175 ++++++++++ crates/zed2/src/main.rs | 10 +- crates/zed2/src/zed2.rs | 41 ++- 13 files changed, 447 insertions(+), 239 deletions(-) create mode 100644 crates/zed2/src/app_menus.rs diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index 64e46549fd6c7b9ae2576ca68d7c0f2af52b750e..cc0095bca94f30f6d65866c640092cb384d2cce3 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -993,7 +993,7 @@ mod tests { use super::*; use crate::display_map::inlay_map::InlayMap; use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; - use gpui::{div, font, px, Element, Platform as _}; + use gpui::{div, font, px, Element}; use multi_buffer::MultiBuffer; use rand::prelude::*; use settings::SettingsStore; @@ -1185,11 +1185,7 @@ mod tests { fn test_blocks_on_wrapped_lines(cx: &mut gpui::TestAppContext) { cx.update(|cx| init_test(cx)); - let font_id = cx - .test_platform - .text_system() - .font_id(&font("Helvetica")) - .unwrap(); + let font_id = cx.text_system().font_id(&font("Helvetica")).unwrap(); let text = "one two three\nfour five six\nseven eight"; diff --git a/crates/editor2/src/display_map/wrap_map.rs b/crates/editor2/src/display_map/wrap_map.rs index a2ac0ec849bfb9b26983c897a2ae3cc2ebd9878c..ca9db7754baec0b1477c19d0f8efaa98fd8f14c3 100644 --- a/crates/editor2/src/display_map/wrap_map.rs +++ b/crates/editor2/src/display_map/wrap_map.rs @@ -1032,7 +1032,7 @@ mod tests { display_map::{fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap}, MultiBuffer, }; - use gpui::{font, px, test::observe, Platform}; + use gpui::{font, px, test::observe}; use rand::prelude::*; use settings::SettingsStore; use smol::stream::StreamExt; diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 424da8987eb6d673f0e789d4b8ae8b1620967045..b5f156f494936cba312d9b44365d1a903a803c4e 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -12,7 +12,7 @@ use futures::StreamExt; use gpui::{ div, serde_json::{self, json}, - Div, Flatten, Platform, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, + Div, Flatten, TestAppContext, VisualTestContext, WindowBounds, WindowOptions, }; use indoc::indoc; use language::{ @@ -3238,9 +3238,7 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) { the lazy dog"}); cx.update_editor(|e, cx| e.copy(&Copy, cx)); assert_eq!( - cx.test_platform - .read_from_clipboard() - .map(|item| item.text().to_owned()), + cx.read_from_clipboard().map(|item| item.text().to_owned()), Some("fox jumps over\n".to_owned()) ); diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 74712feb287468ef67917235b544ffd85ecf8400..79beca75da7f89e83ff2dcd9079f45c29ff153e2 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -15,10 +15,10 @@ use smol::future::FutureExt; pub use test_context::*; use crate::{ - current_platform, image_cache::ImageCache, Action, ActionRegistry, Any, AnyView, - AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, + current_platform, image_cache::ImageCache, init_app_menus, Action, ActionRegistry, Any, + AnyView, AnyWindowHandle, AppMetadata, AssetSource, BackgroundExecutor, ClipboardItem, Context, DispatchPhase, DisplayId, Entity, EventEmitter, FocusEvent, FocusHandle, FocusId, - ForegroundExecutor, KeyBinding, Keymap, LayoutId, PathPromptOptions, Pixels, Platform, + ForegroundExecutor, KeyBinding, Keymap, LayoutId, Menu, PathPromptOptions, Pixels, Platform, PlatformDisplay, Point, Render, SharedString, SubscriberSet, Subscription, SvgRenderer, Task, TextStyle, TextStyleRefinement, TextSystem, View, ViewContext, Window, WindowContext, WindowHandle, WindowId, @@ -278,6 +278,8 @@ impl AppContext { }), }); + init_app_menus(platform.as_ref(), &mut *app.borrow_mut()); + platform.on_quit(Box::new({ let cx = app.clone(); move || { @@ -1059,6 +1061,19 @@ impl AppContext { .contains_key(&action.as_any().type_id()) } + pub fn set_menus(&mut self, menus: Vec) { + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| { + cx.platform + .set_menus(menus, Some(&cx.window.current_frame.dispatch_tree)); + }) + .ok(); + } else { + self.platform.set_menus(menus, None); + } + } + pub fn dispatch_action(&mut self, action: &dyn Action) { self.propagate_event = true; diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index a9403de9bce63a62dfedf455d997de34dff4d856..cbd70e52ffab4d539abfe6a177f8a112825b8402 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -1,9 +1,10 @@ use crate::{ div, Action, AnyView, AnyWindowHandle, AppCell, AppContext, AsyncAppContext, - BackgroundExecutor, Bounds, Context, Div, Entity, EventEmitter, ForegroundExecutor, InputEvent, - KeyDownEvent, Keystroke, Model, ModelContext, Pixels, PlatformWindow, Point, Render, Result, - Size, Task, TestDispatcher, TestPlatform, TestWindow, TestWindowHandlers, View, ViewContext, - VisualContext, WindowBounds, WindowContext, WindowHandle, WindowOptions, + BackgroundExecutor, Bounds, ClipboardItem, Context, Div, Entity, EventEmitter, + ForegroundExecutor, InputEvent, KeyDownEvent, Keystroke, Model, ModelContext, Pixels, Platform, + PlatformWindow, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform, TestWindow, + TestWindowHandlers, TextSystem, View, ViewContext, VisualContext, WindowBounds, WindowContext, + WindowHandle, WindowOptions, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt}; @@ -16,6 +17,7 @@ pub struct TestAppContext { pub foreground_executor: ForegroundExecutor, pub dispatcher: TestDispatcher, pub test_platform: Rc, + text_system: Arc, } impl Context for TestAppContext { @@ -82,6 +84,7 @@ impl TestAppContext { let platform = TestPlatform::new(background_executor.clone(), foreground_executor.clone()); let asset_source = Arc::new(()); let http_client = util::http::FakeHttpClient::with_404_response(); + let text_system = Arc::new(TextSystem::new(platform.text_system())); Self { app: AppContext::new(platform.clone(), asset_source, http_client), @@ -89,6 +92,7 @@ impl TestAppContext { foreground_executor, dispatcher: dispatcher.clone(), test_platform: platform, + text_system, } } @@ -155,6 +159,18 @@ impl TestAppContext { (view, Box::leak(cx)) } + pub fn text_system(&self) -> &Arc { + &self.text_system + } + + pub fn write_to_clipboard(&self, item: ClipboardItem) { + self.test_platform.write_to_clipboard(item) + } + + pub fn read_from_clipboard(&self) -> Option { + self.test_platform.read_from_clipboard() + } + pub fn simulate_new_path_selection( &self, select_path: impl FnOnce(&std::path::Path) -> Option, diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 96be670af7dbc92af50571ab228dcaa663f90a75..7bcd91a5e07dbc03a52fedd6837b9e06c542c6f4 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -6,10 +6,10 @@ mod mac; mod test; use crate::{ - point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, - FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, LineLayout, - Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, - SharedString, Size, TaskLabel, + point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, DispatchTree, + Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, + LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, + Scene, SharedString, Size, TaskLabel, }; use anyhow::{anyhow, bail}; use async_task::Runnable; @@ -46,7 +46,7 @@ pub(crate) fn current_platform() -> Rc { Rc::new(MacPlatform::new()) } -pub trait Platform: 'static { +pub(crate) trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; fn foreground_executor(&self) -> ForegroundExecutor; fn text_system(&self) -> Arc; @@ -92,6 +92,7 @@ pub trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); + fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>); fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); diff --git a/crates/gpui2/src/platform/app_menu.rs b/crates/gpui2/src/platform/app_menu.rs index bfac1366b286ce58f1c423a2da13157881e7c3c8..10fe2cf33ae6d8b830efc92f72bba7f5157de4b9 100644 --- a/crates/gpui2/src/platform/app_menu.rs +++ b/crates/gpui2/src/platform/app_menu.rs @@ -52,7 +52,7 @@ pub enum OsAction { Redo, } -pub(crate) fn init(platform: &dyn Platform, cx: &mut AppContext) { +pub(crate) fn init_app_menus(platform: &dyn Platform, cx: &mut AppContext) { platform.on_will_open_app_menu(Box::new({ let cx = cx.to_async(); move || { diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index d7fc37f0def0736d189a891c733ed8e09997be49..8a5ee676f7c9e76364e6aea27a9b064a917cca36 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,8 +1,8 @@ -use super::BoolExt; +use super::{events::key_to_native, BoolExt}; use crate::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, - ForegroundExecutor, InputEvent, KeystrokeMatcher, MacDispatcher, MacDisplay, MacDisplayLinker, - MacTextSystem, MacWindow, MenuItem, PathPromptOptions, Platform, PlatformDisplay, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, + DisplayId, ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, + MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; use anyhow::anyhow; @@ -10,10 +10,10 @@ use block::ConcreteBlock; use cocoa::{ appkit::{ NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular, - NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, NSPasteboardTypeString, - NSSavePanel, NSWindow, + NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard, + NSPasteboardTypeString, NSSavePanel, NSWindow, }, - base::{id, nil, BOOL, YES}, + base::{id, nil, selector, BOOL, YES}, foundation::{ NSArray, NSAutoreleasePool, NSBundle, NSData, NSInteger, NSProcessInfo, NSString, NSUInteger, NSURL, @@ -201,151 +201,155 @@ impl MacPlatform { } } - // unsafe fn create_menu_bar( - // &self, - // menus: Vec, - // delegate: id, - // actions: &mut Vec>, - // keystroke_matcher: &KeymapMatcher, - // ) -> id { - // let application_menu = NSMenu::new(nil).autorelease(); - // application_menu.setDelegate_(delegate); - - // for menu_config in menus { - // let menu = NSMenu::new(nil).autorelease(); - // menu.setTitle_(ns_string(menu_config.name)); - // menu.setDelegate_(delegate); - - // for item_config in menu_config.items { - // menu.addItem_(self.create_menu_item( - // item_config, - // delegate, - // actions, - // keystroke_matcher, - // )); - // } - - // let menu_item = NSMenuItem::new(nil).autorelease(); - // menu_item.setSubmenu_(menu); - // application_menu.addItem_(menu_item); - - // if menu_config.name == "Window" { - // let app: id = msg_send![APP_CLASS, sharedApplication]; - // app.setWindowsMenu_(menu); - // } - // } - - // application_menu - // } + unsafe fn create_menu_bar( + &self, + menus: Vec, + delegate: id, + actions: &mut Vec>, + dispatch_tree: Option<&DispatchTree>, + ) -> id { + let application_menu = NSMenu::new(nil).autorelease(); + application_menu.setDelegate_(delegate); + + for menu_config in menus { + let menu = NSMenu::new(nil).autorelease(); + menu.setTitle_(ns_string(menu_config.name)); + menu.setDelegate_(delegate); + + for item_config in menu_config.items { + menu.addItem_(self.create_menu_item(item_config, delegate, actions, dispatch_tree)); + } + + let menu_item = NSMenuItem::new(nil).autorelease(); + menu_item.setSubmenu_(menu); + application_menu.addItem_(menu_item); + + if menu_config.name == "Window" { + let app: id = msg_send![APP_CLASS, sharedApplication]; + app.setWindowsMenu_(menu); + } + } + + application_menu + } unsafe fn create_menu_item( &self, item: MenuItem, delegate: id, actions: &mut Vec>, - keystroke_matcher: &KeystrokeMatcher, + dispatch_tree: Option<&DispatchTree>, ) -> id { - todo!() - // match item { - // MenuItem::Separator => NSMenuItem::separatorItem(nil), - // MenuItem::Action { - // name, - // action, - // os_action, - // } => { - // // TODO - // let keystrokes = keystroke_matcher - // .bindings_for_action(action.id()) - // .find(|binding| binding.action().eq(action.as_ref())) - // .map(|binding| binding.keystrokes()); - // let selector = match os_action { - // Some(crate::OsAction::Cut) => selector("cut:"), - // Some(crate::OsAction::Copy) => selector("copy:"), - // Some(crate::OsAction::Paste) => selector("paste:"), - // Some(crate::OsAction::SelectAll) => selector("selectAll:"), - // Some(crate::OsAction::Undo) => selector("undo:"), - // Some(crate::OsAction::Redo) => selector("redo:"), - // None => selector("handleGPUIMenuItem:"), - // }; - - // let item; - // if let Some(keystrokes) = keystrokes { - // if keystrokes.len() == 1 { - // let keystroke = &keystrokes[0]; - // let mut mask = NSEventModifierFlags::empty(); - // for (modifier, flag) in &[ - // (keystroke.cmd, NSEventModifierFlags::NSCommandKeyMask), - // (keystroke.ctrl, NSEventModifierFlags::NSControlKeyMask), - // (keystroke.alt, NSEventModifierFlags::NSAlternateKeyMask), - // (keystroke.shift, NSEventModifierFlags::NSShiftKeyMask), - // ] { - // if *modifier { - // mask |= *flag; - // } - // } - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(key_to_native(&keystroke.key).as_ref()), - // ) - // .autorelease(); - // item.setKeyEquivalentModifierMask_(mask); - // } - // // For multi-keystroke bindings, render the keystroke as part of the title. - // else { - // use std::fmt::Write; - - // let mut name = format!("{name} ["); - // for (i, keystroke) in keystrokes.iter().enumerate() { - // if i > 0 { - // name.push(' '); - // } - // write!(&mut name, "{}", keystroke).unwrap(); - // } - // name.push(']'); - - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(&name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - // } else { - // item = NSMenuItem::alloc(nil) - // .initWithTitle_action_keyEquivalent_( - // ns_string(name), - // selector, - // ns_string(""), - // ) - // .autorelease(); - // } - - // let tag = actions.len() as NSInteger; - // let _: () = msg_send![item, setTag: tag]; - // actions.push(action); - // item - // } - // MenuItem::Submenu(Menu { name, items }) => { - // let item = NSMenuItem::new(nil).autorelease(); - // let submenu = NSMenu::new(nil).autorelease(); - // submenu.setDelegate_(delegate); - // for item in items { - // submenu.addItem_(self.create_menu_item( - // item, - // delegate, - // actions, - // keystroke_matcher, - // )); - // } - // item.setSubmenu_(submenu); - // item.setTitle_(ns_string(name)); - // item - // } - // } + match item { + MenuItem::Separator => NSMenuItem::separatorItem(nil), + MenuItem::Action { + name, + action, + os_action, + } => { + let bindings = dispatch_tree + .map(|tree| tree.bindings_for_action(action.as_ref(), &tree.context_stack)) + .unwrap_or_default(); + let keystrokes = bindings + .iter() + .find(|binding| binding.action().partial_eq(action.as_ref())) + .map(|binding| binding.keystrokes()); + + let selector = match os_action { + Some(crate::OsAction::Cut) => selector("cut:"), + Some(crate::OsAction::Copy) => selector("copy:"), + Some(crate::OsAction::Paste) => selector("paste:"), + Some(crate::OsAction::SelectAll) => selector("selectAll:"), + Some(crate::OsAction::Undo) => selector("undo:"), + Some(crate::OsAction::Redo) => selector("redo:"), + None => selector("handleGPUIMenuItem:"), + }; + + let item; + if let Some(keystrokes) = keystrokes { + if keystrokes.len() == 1 { + let keystroke = &keystrokes[0]; + let mut mask = NSEventModifierFlags::empty(); + for (modifier, flag) in &[ + ( + keystroke.modifiers.command, + NSEventModifierFlags::NSCommandKeyMask, + ), + ( + keystroke.modifiers.control, + NSEventModifierFlags::NSControlKeyMask, + ), + ( + keystroke.modifiers.alt, + NSEventModifierFlags::NSAlternateKeyMask, + ), + ( + keystroke.modifiers.shift, + NSEventModifierFlags::NSShiftKeyMask, + ), + ] { + if *modifier { + mask |= *flag; + } + } + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(key_to_native(&keystroke.key).as_ref()), + ) + .autorelease(); + item.setKeyEquivalentModifierMask_(mask); + } + // For multi-keystroke bindings, render the keystroke as part of the title. + else { + use std::fmt::Write; + + let mut name = format!("{name} ["); + for (i, keystroke) in keystrokes.iter().enumerate() { + if i > 0 { + name.push(' '); + } + write!(&mut name, "{}", keystroke).unwrap(); + } + name.push(']'); + + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(&name), + selector, + ns_string(""), + ) + .autorelease(); + } + } else { + item = NSMenuItem::alloc(nil) + .initWithTitle_action_keyEquivalent_( + ns_string(name), + selector, + ns_string(""), + ) + .autorelease(); + } + + let tag = actions.len() as NSInteger; + let _: () = msg_send![item, setTag: tag]; + actions.push(action); + item + } + MenuItem::Submenu(Menu { name, items }) => { + let item = NSMenuItem::new(nil).autorelease(); + let submenu = NSMenu::new(nil).autorelease(); + submenu.setDelegate_(delegate); + for item in items { + submenu.addItem_(self.create_menu_item(item, delegate, actions, dispatch_tree)); + } + item.setSubmenu_(submenu); + item.setTitle_(ns_string(name)); + item + } + } } } @@ -633,6 +637,18 @@ impl Platform for MacPlatform { self.0.lock().event = Some(callback); } + fn on_app_menu_action(&self, callback: Box) { + self.0.lock().menu_command = Some(callback); + } + + fn on_will_open_app_menu(&self, callback: Box) { + self.0.lock().will_open_menu = Some(callback); + } + + fn on_validate_app_menu_command(&self, callback: Box bool>) { + self.0.lock().validate_menu_command = Some(callback); + } + fn os_name(&self) -> &'static str { "macOS" } @@ -675,6 +691,15 @@ impl Platform for MacPlatform { } } + fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>) { + unsafe { + let app: id = msg_send![APP_CLASS, sharedApplication]; + let mut state = self.0.lock(); + let actions = &mut state.menu_actions; + app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, dispatch_tree)); + } + } + fn local_timezone(&self) -> UtcOffset { unsafe { let local_timezone: id = msg_send![class!(NSTimeZone), localTimeZone]; @@ -683,32 +708,6 @@ impl Platform for MacPlatform { } } - fn on_app_menu_action(&self, callback: Box) { - self.0.lock().menu_command = Some(callback); - } - - fn on_will_open_app_menu(&self, callback: Box) { - self.0.lock().will_open_menu = Some(callback); - } - - fn on_validate_app_menu_command(&self, callback: Box bool>) { - self.0.lock().validate_menu_command = Some(callback); - } - - // fn set_menus(&self, menus: Vec, keystroke_matcher: &KeymapMatcher) { - // unsafe { - // let app: id = msg_send![APP_CLASS, sharedApplication]; - // let mut state = self.0.lock(); - // let actions = &mut state.menu_actions; - // app.setMainMenu_(self.create_menu_bar( - // menus, - // app.delegate(), - // actions, - // keystroke_matcher, - // )); - // } - // } - fn path_for_auxiliary_executable(&self, name: &str) -> Result { unsafe { let bundle: id = NSBundle::mainBundle(); diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 952a9a96cac98e85863931f3e726d71b85e58896..b2a9279df4fbbaee20a507d3c795d596fa142a2a 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -1,6 +1,7 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, - Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, DisplayId, + ForegroundExecutor, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, + WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -205,6 +206,10 @@ impl Platform for TestPlatform { unimplemented!() } + fn set_menus(&self, _menus: Vec, _dispatch_tree: Option<&DispatchTree>) { + unimplemented!() + } + fn on_app_menu_action(&self, _callback: Box) { unimplemented!() } diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index c28e281895771a398e8a214961687df5002b1ccd..590079c51b52fe77a2c83ec4a862b27a0202ad1a 100644 --- a/crates/settings2/src/settings_file.rs +++ b/crates/settings2/src/settings_file.rs @@ -132,39 +132,3 @@ pub fn load_default_keymap(cx: &mut AppContext) { // KeymapFile::load_asset(asset_path, cx).unwrap(); // } } - -pub fn handle_keymap_file_changes( - mut user_keymap_file_rx: mpsc::UnboundedReceiver, - cx: &mut AppContext, -) { - cx.spawn(move |cx| async move { - // let mut settings_subscription = None; - while let Some(user_keymap_content) = user_keymap_file_rx.next().await { - if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() { - cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok(); - - // todo!() - // let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); - // drop(settings_subscription); - // settings_subscription = Some(cx.update(|cx| { - // cx.observe_global::(move |cx| { - // let new_base_keymap = *settings::get::(cx); - // if new_base_keymap != old_base_keymap { - // old_base_keymap = new_base_keymap.clone(); - // reload_keymaps(cx, &keymap_content); - // } - // }) - // })); - } - } - }) - .detach(); -} - -fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { - // todo!() - // cx.clear_bindings(); - load_default_keymap(cx); - keymap_content.clone().add_to_cx(cx).log_err(); - // cx.set_menus(menus::menus()); -} diff --git a/crates/zed2/src/app_menus.rs b/crates/zed2/src/app_menus.rs new file mode 100644 index 0000000000000000000000000000000000000000..70b04e8f9be774d3e7d28e610f65bbabfb80c9b0 --- /dev/null +++ b/crates/zed2/src/app_menus.rs @@ -0,0 +1,175 @@ +use gpui::{Menu, MenuItem, OsAction}; + +#[cfg(target_os = "macos")] +pub fn app_menus() -> Vec> { + vec![ + Menu { + name: "Zed", + items: vec![ + MenuItem::action("About Zed…", super::About), + MenuItem::action("Check for Updates", auto_update::Check), + MenuItem::separator(), + MenuItem::submenu(Menu { + name: "Preferences", + items: vec![ + MenuItem::action("Open Settings", super::OpenSettings), + MenuItem::action("Open Key Bindings", super::OpenKeymap), + MenuItem::action("Open Default Settings", super::OpenDefaultSettings), + MenuItem::action("Open Default Key Bindings", super::OpenDefaultKeymap), + MenuItem::action("Open Local Settings", super::OpenLocalSettings), + MenuItem::action("Select Theme", theme_selector::Toggle), + ], + }), + MenuItem::action("Install CLI", install_cli::Install), + MenuItem::separator(), + MenuItem::action("Hide Zed", super::Hide), + MenuItem::action("Hide Others", super::HideOthers), + MenuItem::action("Show All", super::ShowAll), + MenuItem::action("Quit", super::Quit), + ], + }, + Menu { + name: "File", + items: vec![ + MenuItem::action("New", workspace::NewFile), + MenuItem::action("New Window", workspace::NewWindow), + MenuItem::separator(), + MenuItem::action("Open…", workspace::Open), + // MenuItem::action("Open Recent...", recent_projects::OpenRecent), + MenuItem::separator(), + MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), + MenuItem::action("Save", workspace::Save { save_intent: None }), + MenuItem::action("Save As…", workspace::SaveAs), + MenuItem::action("Save All", workspace::SaveAll { save_intent: None }), + MenuItem::action( + "Close Editor", + workspace::CloseActiveItem { save_intent: None }, + ), + MenuItem::action("Close Window", workspace::CloseWindow), + ], + }, + Menu { + name: "Edit", + items: vec![ + MenuItem::os_action("Undo", editor::Undo, OsAction::Undo), + MenuItem::os_action("Redo", editor::Redo, OsAction::Redo), + MenuItem::separator(), + MenuItem::os_action("Cut", editor::Cut, OsAction::Cut), + MenuItem::os_action("Copy", editor::Copy, OsAction::Copy), + MenuItem::os_action("Paste", editor::Paste, OsAction::Paste), + MenuItem::separator(), + MenuItem::action("Find", search::buffer_search::Deploy { focus: true }), + MenuItem::action("Find In Project", workspace::NewSearch), + MenuItem::separator(), + MenuItem::action("Toggle Line Comment", editor::ToggleComments::default()), + MenuItem::action("Emoji & Symbols", editor::ShowCharacterPalette), + ], + }, + Menu { + name: "Selection", + items: vec![ + MenuItem::os_action("Select All", editor::SelectAll, OsAction::SelectAll), + MenuItem::action("Expand Selection", editor::SelectLargerSyntaxNode), + MenuItem::action("Shrink Selection", editor::SelectSmallerSyntaxNode), + MenuItem::separator(), + MenuItem::action("Add Cursor Above", editor::AddSelectionAbove), + MenuItem::action("Add Cursor Below", editor::AddSelectionBelow), + MenuItem::action( + "Select Next Occurrence", + editor::SelectNext { + replace_newest: false, + }, + ), + MenuItem::separator(), + MenuItem::action("Move Line Up", editor::MoveLineUp), + MenuItem::action("Move Line Down", editor::MoveLineDown), + MenuItem::action("Duplicate Selection", editor::DuplicateLine), + ], + }, + Menu { + name: "View", + items: vec![ + MenuItem::action("Zoom In", super::IncreaseBufferFontSize), + MenuItem::action("Zoom Out", super::DecreaseBufferFontSize), + MenuItem::action("Reset Zoom", super::ResetBufferFontSize), + MenuItem::separator(), + MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), + MenuItem::action("Toggle Right Dock", workspace::ToggleRightDock), + MenuItem::action("Toggle Bottom Dock", workspace::ToggleBottomDock), + MenuItem::action("Close All Docks", workspace::CloseAllDocks), + MenuItem::submenu(Menu { + name: "Editor Layout", + items: vec![ + MenuItem::action("Split Up", workspace::SplitUp), + MenuItem::action("Split Down", workspace::SplitDown), + MenuItem::action("Split Left", workspace::SplitLeft), + MenuItem::action("Split Right", workspace::SplitRight), + ], + }), + MenuItem::separator(), + MenuItem::action("Project Panel", project_panel::ToggleFocus), + MenuItem::action("Command Palette", command_palette::Toggle), + MenuItem::action("Diagnostics", diagnostics::Deploy), + MenuItem::separator(), + ], + }, + Menu { + name: "Go", + items: vec![ + MenuItem::action("Back", workspace::GoBack), + MenuItem::action("Forward", workspace::GoForward), + MenuItem::separator(), + MenuItem::action("Go to File", file_finder::Toggle), + // MenuItem::action("Go to Symbol in Project", project_symbols::Toggle), + MenuItem::action("Go to Symbol in Editor", outline::Toggle), + MenuItem::action("Go to Definition", editor::GoToDefinition), + MenuItem::action("Go to Type Definition", editor::GoToTypeDefinition), + MenuItem::action("Find All References", editor::FindAllReferences), + MenuItem::action("Go to Line/Column", go_to_line::Toggle), + MenuItem::separator(), + MenuItem::action("Next Problem", editor::GoToDiagnostic), + MenuItem::action("Previous Problem", editor::GoToPrevDiagnostic), + ], + }, + Menu { + name: "Window", + items: vec![ + MenuItem::action("Minimize", super::Minimize), + MenuItem::action("Zoom", super::Zoom), + MenuItem::separator(), + ], + }, + Menu { + name: "Help", + items: vec![ + MenuItem::action("Command Palette", command_palette::Toggle), + MenuItem::separator(), + MenuItem::action("View Telemetry", crate::OpenTelemetryLog), + MenuItem::action("View Dependency Licenses", crate::OpenLicenses), + MenuItem::action("Show Welcome", workspace::Welcome), + MenuItem::separator(), + // todo!(): Needs `feedback2` crate. + // MenuItem::action("Give us feedback", feedback::feedback_editor::GiveFeedback), + // MenuItem::action( + // "Copy System Specs Into Clipboard", + // feedback::CopySystemSpecsIntoClipboard, + // ), + // MenuItem::action("File Bug Report", feedback::FileBugReport), + // MenuItem::action("Request Feature", feedback::RequestFeature), + MenuItem::separator(), + MenuItem::action( + "Documentation", + crate::OpenBrowser { + url: "https://zed.dev/docs".into(), + }, + ), + MenuItem::action( + "Zed Twitter", + crate::OpenBrowser { + url: "https://twitter.com/zeddotdev".into(), + }, + ), + ], + }, + ] +} diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 6ca5d1a805b53ab1dabd43591b7353c0231aa365..7faafb2440c7e311641219461c2d79934702ddb9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -22,8 +22,7 @@ use node_runtime::RealNodeRuntime; use parking_lot::Mutex; use serde::{Deserialize, Serialize}; use settings::{ - default_settings, handle_keymap_file_changes, handle_settings_file_changes, watch_config_file, - Settings, SettingsStore, + default_settings, handle_settings_file_changes, watch_config_file, Settings, SettingsStore, }; use simplelog::ConfigBuilder; use smol::process::Command; @@ -51,8 +50,9 @@ use uuid::Uuid; use welcome::{show_welcome_experience, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed2::{ - build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, - languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, + app_menus, build_window_options, ensure_only_instance, handle_cli_connection, + handle_keymap_file_changes, initialize_workspace, languages, Assets, IsOnlyInstance, + OpenListener, OpenRequest, }; mod open_listener; @@ -224,7 +224,7 @@ fn main() { // feedback::init(cx); welcome::init(cx); - // cx.set_menus(menus::menus()); + cx.set_menus(app_menus()); initialize_workspace(app_state.clone(), cx); if stdout_is_a_pty() { diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index abe8e7a86f19802f2ab8e7347ba87ba4b6a08050..7e69a2aee3ec799a7e5c6876da39e45386c6487f 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,11 +1,13 @@ #![allow(unused_variables, unused_mut)] //todo!() +mod app_menus; mod assets; pub mod languages; mod only_instance; mod open_listener; +pub use app_menus::*; pub use assets::*; use breadcrumbs::Breadcrumbs; use collections::VecDeque; @@ -18,8 +20,9 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::{anyhow, Context as _}; +use futures::{channel::mpsc, StreamExt}; use project_panel::ProjectPanel; -use settings::{initial_local_settings_content, Settings}; +use settings::{initial_local_settings_content, load_default_keymap, KeymapFile, Settings}; use std::{borrow::Cow, ops::Deref, sync::Arc}; use terminal_view::terminal_panel::TerminalPanel; use util::{ @@ -561,6 +564,42 @@ fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { .detach(); } +pub fn handle_keymap_file_changes( + mut user_keymap_file_rx: mpsc::UnboundedReceiver, + cx: &mut AppContext, +) { + cx.spawn(move |cx| async move { + // let mut settings_subscription = None; + while let Some(user_keymap_content) = user_keymap_file_rx.next().await { + if let Some(keymap_content) = KeymapFile::parse(&user_keymap_content).log_err() { + cx.update(|cx| reload_keymaps(cx, &keymap_content)).ok(); + + // todo!() + // let mut old_base_keymap = cx.read(|cx| *settings::get::(cx)); + // drop(settings_subscription); + // settings_subscription = Some(cx.update(|cx| { + // cx.observe_global::(move |cx| { + // let new_base_keymap = *settings::get::(cx); + // if new_base_keymap != old_base_keymap { + // old_base_keymap = new_base_keymap.clone(); + // reload_keymaps(cx, &keymap_content); + // } + // }) + // })); + } + } + }) + .detach(); +} + +fn reload_keymaps(cx: &mut AppContext, keymap_content: &KeymapFile) { + // todo!() + // cx.clear_bindings(); + load_default_keymap(cx); + keymap_content.clone().add_to_cx(cx).log_err(); + cx.set_menus(app_menus()); +} + fn open_local_settings_file( workspace: &mut Workspace, _: &OpenLocalSettings, From 2ee0ecb6774f9831f0f07cbddd5d841a5803e834 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 5 Dec 2023 16:52:29 -0800 Subject: [PATCH 37/90] Add back the main structure --- crates/gpui2/src/elements/div.rs | 14 +- crates/terminal2/src/terminal2.rs | 3 +- crates/terminal_view2/src/terminal_element.rs | 353 +++++++++--------- crates/terminal_view2/src/terminal_view.rs | 327 +++++++++------- 4 files changed, 385 insertions(+), 312 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index e653710c0f18307f2b605c968294d436719c483f..aa6a5e32ec516a128e5f790097f3e99307332b2e 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,10 +1,10 @@ use crate::{ - point, px, Action, AnyDrag, AnyDragState, AnyElement, AnyTooltip, AnyView, AppContext, - BorrowAppContext, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, - FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, - Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, - Task, View, Visibility, WindowContext, + green, point, px, red, Action, AnyDrag, AnyDragState, AnyElement, AnyTooltip, AnyView, + AppContext, BorrowAppContext, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, + ElementId, FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, + LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, + Point, Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, + Styled, Task, View, Visibility, WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -1363,7 +1363,7 @@ impl GroupBounds { } pub struct Focusable { - element: E, + pub element: E, } impl FocusableElement for Focusable {} diff --git a/crates/terminal2/src/terminal2.rs b/crates/terminal2/src/terminal2.rs index 197b912b1b29582b5f419250da390c9c8beb1148..6036d65d6e8c59d3aff234952980564ec9f71bec 100644 --- a/crates/terminal2/src/terminal2.rs +++ b/crates/terminal2/src/terminal2.rs @@ -976,7 +976,6 @@ impl Terminal { } pub fn try_sync(&mut self, cx: &mut ModelContext) { - println!("trying to sync"); let term = self.term.clone(); let mut terminal = if let Some(term) = term.try_lock_unfair() { @@ -1235,7 +1234,7 @@ impl Terminal { } ///Scroll the terminal - pub fn scroll_wheel(&mut self, e: ScrollWheelEvent, origin: Point) { + pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Point) { let mouse_mode = self.mouse_mode(e.shift); if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) { diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 96ffbb1ffbf8c9bfab8919edd59429667cb1f3ed..03e98e831d8013608b1483a08c95122978f1ba73 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,10 +1,11 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ black, div, point, px, red, relative, transparent_black, AnyElement, AvailableSpace, Bounds, - Element, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla, - InteractiveElement, InteractiveElementState, IntoElement, LayoutId, ModelContext, Pixels, - Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled, TextRun, TextStyle, - TextSystem, UnderlineStyle, WeakModel, WhiteSpace, WindowContext, + DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, + Hsla, InteractiveElement, InteractiveElementState, IntoElement, LayoutId, ModelContext, + ModifiersChangedEvent, MouseButton, Pixels, Point, Rgba, ShapedLine, Size, + StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, View, + WeakModel, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -26,6 +27,8 @@ use ui::Tooltip; use std::mem; use std::{fmt::Debug, ops::RangeInclusive}; +use crate::TerminalView; + ///The information generated during layout that is necessary for painting pub struct LayoutState { cells: Vec, @@ -146,6 +149,7 @@ impl LayoutRect { ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalElement { terminal: WeakModel, + terminal_view: View, focus: FocusHandle, focused: bool, cursor_visible: bool, @@ -164,6 +168,7 @@ impl StatefulInteractiveElement for TerminalElement {} impl TerminalElement { pub fn new( terminal: WeakModel, + terminal_view: View, focus: FocusHandle, focused: bool, cursor_visible: bool, @@ -171,12 +176,15 @@ impl TerminalElement { ) -> TerminalElement { TerminalElement { terminal, + terminal_view, focused, - focus, + focus: focus.clone(), cursor_visible, can_navigate_to_selected_word, interactivity: Default::default(), } + .track_focus(&focus) + .element } //Vec> -> Clip out the parts of the ranges @@ -601,7 +609,25 @@ impl TerminalElement { } } - fn paint_mouse_listeners( + fn register_key_listeners(&self, cx: &mut WindowContext) { + cx.on_key_event({ + let this = self.terminal.clone(); + move |event: &ModifiersChangedEvent, phase, cx| { + if phase != DispatchPhase::Bubble { + return; + } + + let handled = this + .update(cx, |term, _| term.try_modifiers_change(&event.modifiers)) + .ok(); + if handled == Some(true) { + cx.notify(); + } + } + }); + } + + fn register_mouse_listeners( self, origin: Point, mode: TermMode, @@ -611,133 +637,153 @@ impl TerminalElement { let focus = self.focus.clone(); let connection = self.terminal.clone(); - self.on_mouse_down(gpui::MouseButton::Left, { - let connection = connection.clone(); - let focus = focus.clone(); - move |e, cx| { - cx.focus(&focus); - //todo!(context menu) - // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_down(&e, origin); - - cx.notify(); - }) - } - } - }) - .on_drag_event({ - let connection = connection.clone(); - let focus = focus.clone(); - move |e, cx| { - if focus.is_focused(cx) { + let mut this = self + .on_mouse_down(MouseButton::Left, { + let connection = connection.clone(); + let focus = focus.clone(); + move |e, cx| { + dbg!("here"); + cx.focus(&focus); + //todo!(context menu) + // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); if let Some(conn_handle) = connection.upgrade() { conn_handle.update(cx, |terminal, cx| { - terminal.mouse_drag(e, origin, bounds); + terminal.mouse_down(&e, origin); + cx.notify(); }) } } - } - }) - .on_mouse_up( - gpui::MouseButton::Left, - TerminalElement::generic_button_handler( - connection.clone(), - origin, - focus.clone(), - move |terminal, origin, e, cx| { - terminal.mouse_up(&e, origin, cx); - }, - ), - ) - .on_click({ - let connection = connection.clone(); - move |e, cx| { - if e.down.button == gpui::MouseButton::Right { - let mouse_mode = if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, _cx| { - terminal.mouse_mode(e.down.modifiers.shift) + }) + .on_drag_event({ + let connection = connection.clone(); + let focus = focus.clone(); + move |e, cx| { + dbg!("here"); + + if focus.is_focused(cx) { + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + }) + } + } + } + }) + .on_mouse_up( + MouseButton::Left, + TerminalElement::generic_button_handler( + connection.clone(), + origin, + focus.clone(), + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ) + .on_click({ + let connection = connection.clone(); + move |e, cx| { + dbg!("here"); + + if e.down.button == MouseButton::Right { + let mouse_mode = if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, _cx| { + terminal.mouse_mode(e.down.modifiers.shift) + }) + } else { + // If we can't get the model handle, probably can't deploy the context menu + true + }; + if !mouse_mode { + //todo!(context menu) + // view.deploy_context_menu(e.position, cx); + } + } + } + }) + .on_mouse_move({ + let connection = connection.clone(); + let focus = focus.clone(); + move |e, cx| { + dbg!("here"); + + if focus.is_focused(cx) { + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_move(&e, origin); + cx.notify(); + }) + } + } + } + }) + .on_scroll_wheel({ + let connection = connection.clone(); + move |e, cx| { + dbg!("here"); + + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + terminal.scroll_wheel(e, origin); + cx.notify(); }) - } else { - // If we can't get the model handle, probably can't deploy the context menu - true - }; - if !mouse_mode { - //todo!(context menu) - // view.deploy_context_menu(e.position, cx); } } - } - }) - - // .on_move(move |event, _: &mut TerminalView, cx| { - // if cx.is_self_focused() { - // if let Some(conn_handle) = connection.upgrade() { - // conn_handle.update(cx, |terminal, cx| { - // terminal.mouse_move(&event, origin); - // cx.notify(); - // }) - // } - // } - // }) - // .on_scroll(move |event, _: &mut TerminalView, cx| { - // if let Some(conn_handle) = connection.upgrade() { - // conn_handle.update(cx, |terminal, cx| { - // terminal.scroll_wheel(event, origin); - // cx.notify(); - // }) - // } - // }); - - // // Mouse mode handlers: - // // All mouse modes need the extra click handlers - // if mode.intersects(TermMode::MOUSE_MODE) { - // region = region - // .on_down( - // MouseButton::Right, - // TerminalElement::generic_button_handler( - // connection, - // origin, - // move |terminal, origin, e, _cx| { - // terminal.mouse_down(&e, origin); - // }, - // ), - // ) - // .on_down( - // MouseButton::Middle, - // TerminalElement::generic_button_handler( - // connection, - // origin, - // move |terminal, origin, e, _cx| { - // terminal.mouse_down(&e, origin); - // }, - // ), - // ) - // .on_up( - // MouseButton::Right, - // TerminalElement::generic_button_handler( - // connection, - // origin, - // move |terminal, origin, e, cx| { - // terminal.mouse_up(&e, origin, cx); - // }, - // ), - // ) - // .on_up( - // MouseButton::Middle, - // TerminalElement::generic_button_handler( - // connection, - // origin, - // move |terminal, origin, e, cx| { - // terminal.mouse_up(&e, origin, cx); - // }, - // ), - // ) - // } - - // cx.scene().push_mouse_region(region); + }); + + // Mouse mode handlers: + // All mouse modes need the extra click handlers + if mode.intersects(TermMode::MOUSE_MODE) { + this = this + .on_mouse_down( + MouseButton::Right, + TerminalElement::generic_button_handler( + connection.clone(), + origin, + focus.clone(), + move |terminal, origin, e, _cx| { + terminal.mouse_down(&e, origin); + }, + ), + ) + .on_mouse_down( + MouseButton::Middle, + TerminalElement::generic_button_handler( + connection.clone(), + origin, + focus.clone(), + move |terminal, origin, e, _cx| { + terminal.mouse_down(&e, origin); + }, + ), + ) + .on_mouse_up( + MouseButton::Right, + TerminalElement::generic_button_handler( + connection.clone(), + origin, + focus.clone(), + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ) + .on_mouse_up( + MouseButton::Middle, + TerminalElement::generic_button_handler( + connection, + origin, + focus, + move |terminal, origin, e, cx| { + terminal.mouse_up(&e, origin, cx); + }, + ), + ) + } + + this } } @@ -762,11 +808,18 @@ impl Element for TerminalElement { (layout_id, interactive_state) } - fn paint(self, bounds: Bounds, state: &mut Self::State, cx: &mut WindowContext<'_>) { + fn paint( + mut self, + bounds: Bounds, + state: &mut Self::State, + cx: &mut WindowContext<'_>, + ) { let mut layout = self.compute_layout(bounds, cx); let theme = cx.theme(); + let dispatch_context = self.terminal_view.read(cx).dispatch_context(cx); + self.interactivity().key_context = dispatch_context; cx.paint_quad( bounds, Default::default(), @@ -776,10 +829,13 @@ impl Element for TerminalElement { ); let origin = bounds.origin + Point::new(layout.gutter, px(0.)); - let this = self.paint_mouse_listeners(origin, layout.mode, bounds, cx); + let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx); + let interactivity = mem::take(&mut this.interactivity); + + cx.with_z_index(0, |cx| { + interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { + this.register_key_listeners(cx); - this.interactivity - .paint(bounds, bounds.size, state, cx, |_, _, cx| { for rect in &layout.rects { rect.paint(origin, &layout, cx); } @@ -824,47 +880,8 @@ impl Element for TerminalElement { element.draw(origin, Size { width, height }, cx) } }); + }); } - - // todo!() remove? - // fn metadata(&self) -> Option<&dyn std::any::Any> { - // None - // } - - // fn debug( - // &self, - // _: Bounds, - // _: &Self::State, - // _: &Self::PaintState, - // _: &TerminalView, - // _: &gpui::ViewContext, - // ) -> gpui::serde_json::Value { - // json!({ - // "type": "TerminalElement", - // }) - // } - - // fn rect_for_text_range( - // &self, - // _: Range, - // bounds: Bounds, - // _: Bounds, - // layout: &Self::State, - // _: &Self::PaintState, - // _: &TerminalView, - // _: &gpui::ViewContext, - // ) -> Option> { - // // Use the same origin that's passed to `Cursor::paint` in the paint - // // method bove. - // let mut origin = bounds.origin() + point(layout.size.cell_width, 0.); - - // // TODO - Why is it necessary to move downward one line to get correct - // // positioning? I would think that we'd want the same rect that is - // // painted for the cursor. - // origin += point(0., layout.size.line_height); - - // Some(layout.cursor.as_ref()?.bounding_rect(origin)) - // } } impl IntoElement for TerminalElement { diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 5b864f4a5e76420d95a9c864d4a0e1721b81e863..e9e4cd516762d240c191e54e374ccdec9acc05f2 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,10 +9,11 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent, - FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement, - KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, - SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, div, point, px, size, Action, AnyElement, AppContext, Bounds, Div, Element, + EventEmitter, FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, Font, + FontStyle, FontWeight, InputHandler, InteractiveElement, KeyContext, KeyDownEvent, Keystroke, + Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, SharedString, Styled, + Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::Bias; use persistence::TERMINAL_DB; @@ -26,6 +27,7 @@ use terminal::{ Event, MaybeNavigationTarget, Terminal, }; use terminal_element::TerminalElement; +use theme::ThemeSettings; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, @@ -91,6 +93,7 @@ pub struct TerminalView { blink_epoch: usize, can_navigate_to_selected_word: bool, workspace_id: WorkspaceId, + _subscriptions: Vec, } impl EventEmitter for TerminalView {} @@ -262,6 +265,20 @@ impl TerminalView { }) .detach(); + let focus = cx.focus_handle(); + let focus_in = cx.on_focus_in(&focus, |this, cx| { + this.has_new_content = false; + this.terminal.read(cx).focus_in(); + this.blink_cursors(this.blink_epoch, cx); + cx.notify(); + }); + let focus_out = cx.on_focus_out(&focus, |this, cx| { + this.terminal.update(cx, |terminal, _| { + terminal.focus_out(); + }); + cx.notify(); + }); + Self { terminal, has_new_content: true, @@ -274,6 +291,7 @@ impl TerminalView { blink_epoch: 0, can_navigate_to_selected_word: false, workspace_id, + _subscriptions: vec![focus_in, focus_out], } } @@ -303,7 +321,7 @@ impl TerminalView { menu.action("Clear", Box::new(Clear)) .action("Close", Box::new(CloseActiveItem { save_intent: None })) })); - // todo!() + // todo!(context menus) // self.context_menu // .show(position, AnchorCorner::TopLeft, menu_entries, cx); // cx.notify(); @@ -448,6 +466,81 @@ impl TerminalView { }); } } + + fn dispatch_context(&self, cx: &AppContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("Terminal"); + + let mode = self.terminal.read(cx).last_content.mode; + dispatch_context.set( + "screen", + if mode.contains(TermMode::ALT_SCREEN) { + "alt" + } else { + "normal" + }, + ); + + if mode.contains(TermMode::APP_CURSOR) { + dispatch_context.add("DECCKM"); + } + if mode.contains(TermMode::APP_KEYPAD) { + dispatch_context.add("DECPAM"); + } else { + dispatch_context.add("DECPNM"); + } + if mode.contains(TermMode::SHOW_CURSOR) { + dispatch_context.add("DECTCEM"); + } + if mode.contains(TermMode::LINE_WRAP) { + dispatch_context.add("DECAWM"); + } + if mode.contains(TermMode::ORIGIN) { + dispatch_context.add("DECOM"); + } + if mode.contains(TermMode::INSERT) { + dispatch_context.add("IRM"); + } + //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html + if mode.contains(TermMode::LINE_FEED_NEW_LINE) { + dispatch_context.add("LNM"); + } + if mode.contains(TermMode::FOCUS_IN_OUT) { + dispatch_context.add("report_focus"); + } + if mode.contains(TermMode::ALTERNATE_SCROLL) { + dispatch_context.add("alternate_scroll"); + } + if mode.contains(TermMode::BRACKETED_PASTE) { + dispatch_context.add("bracketed_paste"); + } + if mode.intersects(TermMode::MOUSE_MODE) { + dispatch_context.add("any_mouse_reporting"); + } + { + let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { + "click" + } else if mode.contains(TermMode::MOUSE_DRAG) { + "drag" + } else if mode.contains(TermMode::MOUSE_MOTION) { + "motion" + } else { + "off" + }; + dispatch_context.set("mouse_reporting", mouse_reporting); + } + { + let format = if mode.contains(TermMode::SGR_MOUSE) { + "sgr" + } else if mode.contains(TermMode::UTF8_MOUSE) { + "utf8" + } else { + "normal" + }; + dispatch_context.set("mouse_format", format); + }; + dispatch_context + } } fn possible_open_targets( @@ -533,6 +626,7 @@ impl Render for TerminalView { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let terminal_handle = self.terminal.clone().downgrade(); + let this_view = cx.view().clone(); let self_id = cx.entity_id(); let focused = self.focus_handle.is_focused(cx); @@ -555,6 +649,7 @@ impl Render for TerminalView { .on_action(cx.listener(TerminalView::select_all)) .child(TerminalElement::new( terminal_handle, + this_view, self.focus_handle.clone(), focused, self.should_show_cursor(focused, cx), @@ -579,104 +674,14 @@ impl Render for TerminalView { } } -// impl View for TerminalView { -//todo!() -// fn modifiers_changed( -// &mut self, -// event: &ModifiersChangedEvent, -// cx: &mut ViewContext, -// ) -> bool { -// let handled = self -// .terminal() -// .update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); -// if handled { -// cx.notify(); -// } -// handled -// } -// } - -// todo!() -// fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &gpui::AppContext) { -// Self::reset_to_default_keymap_context(keymap); - -// let mode = self.terminal.read(cx).last_content.mode; -// keymap.add_key( -// "screen", -// if mode.contains(TermMode::ALT_SCREEN) { -// "alt" -// } else { -// "normal" -// }, -// ); - -// if mode.contains(TermMode::APP_CURSOR) { -// keymap.add_identifier("DECCKM"); -// } -// if mode.contains(TermMode::APP_KEYPAD) { -// keymap.add_identifier("DECPAM"); -// } else { -// keymap.add_identifier("DECPNM"); -// } -// if mode.contains(TermMode::SHOW_CURSOR) { -// keymap.add_identifier("DECTCEM"); -// } -// if mode.contains(TermMode::LINE_WRAP) { -// keymap.add_identifier("DECAWM"); -// } -// if mode.contains(TermMode::ORIGIN) { -// keymap.add_identifier("DECOM"); -// } -// if mode.contains(TermMode::INSERT) { -// keymap.add_identifier("IRM"); -// } -// //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html -// if mode.contains(TermMode::LINE_FEED_NEW_LINE) { -// keymap.add_identifier("LNM"); -// } -// if mode.contains(TermMode::FOCUS_IN_OUT) { -// keymap.add_identifier("report_focus"); -// } -// if mode.contains(TermMode::ALTERNATE_SCROLL) { -// keymap.add_identifier("alternate_scroll"); -// } -// if mode.contains(TermMode::BRACKETED_PASTE) { -// keymap.add_identifier("bracketed_paste"); -// } -// if mode.intersects(TermMode::MOUSE_MODE) { -// keymap.add_identifier("any_mouse_reporting"); -// } -// { -// let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) { -// "click" -// } else if mode.contains(TermMode::MOUSE_DRAG) { -// "drag" -// } else if mode.contains(TermMode::MOUSE_MOTION) { -// "motion" -// } else { -// "off" -// }; -// keymap.add_key("mouse_reporting", mouse_reporting); -// } -// { -// let format = if mode.contains(TermMode::SGR_MOUSE) { -// "sgr" -// } else if mode.contains(TermMode::UTF8_MOUSE) { -// "utf8" -// } else { -// "normal" -// }; -// keymap.add_key("mouse_format", format); -// } -// } - +//todo!(Implement IME) impl InputHandler for TerminalView { fn text_for_range( &mut self, range: std::ops::Range, cx: &mut ViewContext, ) -> Option { - todo!() + None } fn selected_text_range( @@ -696,13 +701,11 @@ impl InputHandler for TerminalView { } } - fn marked_text_range(&self, cx: &mut ViewContext) -> Option> { - todo!() + fn marked_text_range(&self, _cx: &mut ViewContext) -> Option> { + None } - fn unmark_text(&mut self, cx: &mut ViewContext) { - todo!() - } + fn unmark_text(&mut self, _cx: &mut ViewContext) {} fn replace_text_in_range( &mut self, @@ -717,21 +720,75 @@ impl InputHandler for TerminalView { fn replace_and_mark_text_in_range( &mut self, - range: Option>, - new_text: &str, - new_selected_range: Option>, - cx: &mut ViewContext, + _range: Option>, + _new_text: &str, + _new_selected_range: Option>, + _cx: &mut ViewContext, ) { - todo!() } + // todo!(Check that this works correctly, why aren't we reading the range?) fn bounds_for_range( &mut self, - range_utf16: std::ops::Range, - element_bounds: gpui::Bounds, + _range_utf16: std::ops::Range, + bounds: gpui::Bounds, cx: &mut ViewContext, ) -> Option> { - todo!() + let settings = ThemeSettings::get_global(cx).clone(); + + let buffer_font_size = settings.buffer_font_size(cx); + + let terminal_settings = TerminalSettings::get_global(cx); + let font_family = terminal_settings + .font_family + .as_ref() + .map(|string| string.clone().into()) + .unwrap_or(settings.buffer_font.family); + + let line_height = terminal_settings + .line_height + .value() + .to_pixels(cx.rem_size()); + + let font_size = terminal_settings.font_size.clone(); + let features = terminal_settings + .font_features + .clone() + .unwrap_or(settings.buffer_font.features.clone()); + + let font_size = + font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); + + let font_id = cx + .text_system() + .font_id(&Font { + family: font_family, + style: FontStyle::Normal, + weight: FontWeight::NORMAL, + features, + }) + .unwrap(); + + let cell_width = cx + .text_system() + .advance(font_id, font_size, 'm') + .unwrap() + .width; + + let mut origin = bounds.origin + point(cell_width, px(0.)); + + // TODO - Why is it necessary to move downward one line to get correct + // positioning? I would think that we'd want the same rect that is + // painted for the cursor. + origin += point(px(0.), line_height); + + let cursor = Bounds { + origin, + //todo!(correctly calculate this width and height based on the text the line is over) + size: size(cell_width, line_height), + }; + + Some(cursor) } } @@ -776,7 +833,7 @@ impl Item for TerminalView { false } - // todo!() + // todo!(search) // fn as_searchable(&self, handle: &View) -> Option> { // Some(Box::new(handle.clone())) // } @@ -806,22 +863,23 @@ impl Item for TerminalView { let window = cx.window_handle(); cx.spawn(|pane, mut cx| async move { let cwd = None; - // todo!() - // TERMINAL_DB - // .get_working_directory(item_id, workspace_id) - // .log_err() - // .flatten() - // .or_else(|| { - // cx.read(|cx| { - // let strategy = TerminalSettings::get_global(cx).working_directory.clone(); - // workspace - // .upgrade() - // .map(|workspace| { - // get_working_directory(workspace.read(cx), cx, strategy) - // }) - // .flatten() - // }) - // }); + TERMINAL_DB + .get_working_directory(item_id, workspace_id) + .log_err() + .flatten() + .or_else(|| { + cx.update(|_, cx| { + let strategy = TerminalSettings::get_global(cx).working_directory.clone(); + workspace + .upgrade() + .map(|workspace| { + get_working_directory(workspace.read(cx), cx, strategy) + }) + .flatten() + }) + .ok() + .flatten() + }); let terminal = project.update(&mut cx, |project, cx| { project.create_terminal(cwd, window, cx) @@ -833,14 +891,13 @@ impl Item for TerminalView { } fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - // todo!() - // cx.background() - // .spawn(TERMINAL_DB.update_workspace_id( - // workspace.database_id(), - // self.workspace_id, - // cx.view_id(), - // )) - // .detach(); + cx.background_executor() + .spawn(TERMINAL_DB.update_workspace_id( + workspace.database_id(), + self.workspace_id, + cx.entity_id().as_u64(), + )) + .detach(); self.workspace_id = workspace.database_id(); } } From d8757845a988bc8d5f1de338962ad8fcf1d7129e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 20:21:35 -0500 Subject: [PATCH 38/90] Wire up `NewFile` action --- crates/editor2/src/editor.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index a77e1dcc3b9621080e4e78ef94f80fff2c18112e..4b14ec42f1ed11b73220b4e785c4ceb1ad7cae45 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -92,6 +92,7 @@ use std::{ ops::{ControlFlow, Deref, DerefMut, Range, RangeInclusive}, path::Path, sync::Arc, + sync::Weak, time::{Duration, Instant}, }; pub use sum_tree::Bias; @@ -420,6 +421,16 @@ pub fn init(cx: &mut AppContext) { }, ) .detach(); + + cx.on_action(move |_: &workspace::NewFile, cx| { + let app_state = cx.global::>(); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + }); } trait InvalidationRegion { From ed31d36ac1013c06d07e142092b9618cbc7fbf4a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 20:24:01 -0500 Subject: [PATCH 39/90] Wire up `NewWindow` action --- crates/editor2/src/editor.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 4b14ec42f1ed11b73220b4e785c4ceb1ad7cae45..3fdccb2de42f578c1a9857a9b25403119f617e00 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -431,6 +431,15 @@ pub fn init(cx: &mut AppContext) { .detach(); } }); + cx.on_action(move |_: &workspace::NewWindow, cx| { + let app_state = cx.global::>(); + if let Some(app_state) = app_state.upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + }) } trait InvalidationRegion { From 5660c8f655359286c4f3138c925dec847b877e5a Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 5 Dec 2023 20:24:43 -0500 Subject: [PATCH 40/90] Add missing semicolon --- crates/editor2/src/editor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3fdccb2de42f578c1a9857a9b25403119f617e00..07f00198de2dcb4a5281b868e1c1727c83fe3c43 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -439,7 +439,7 @@ pub fn init(cx: &mut AppContext) { }) .detach(); } - }) + }); } trait InvalidationRegion { From 735f2029e9bbb4edad1bba4a3709da37529ade9c Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 5 Dec 2023 17:31:33 -0800 Subject: [PATCH 41/90] Add more debugging --- crates/gpui2/src/elements/div.rs | 40 ++++++++-- crates/terminal_view2/src/terminal_element.rs | 80 +++++++++---------- crates/terminal_view2/src/terminal_view.rs | 26 +++--- 3 files changed, 85 insertions(+), 61 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index aa6a5e32ec516a128e5f790097f3e99307332b2e..168be4d094baece74959ffd8306eedbb77ebd030 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,10 +1,10 @@ use crate::{ - green, point, px, red, Action, AnyDrag, AnyDragState, AnyElement, AnyTooltip, AnyView, - AppContext, BorrowAppContext, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, - ElementId, FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, - LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, - Point, Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, - Styled, Task, View, Visibility, WindowContext, + point, px, Action, AnyDrag, AnyDragState, AnyElement, AnyTooltip, AnyView, AppContext, + BorrowAppContext, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, + FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, + MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, + Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, + Task, View, Visibility, WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -95,6 +95,32 @@ pub trait InteractiveElement: Sized + Element { self } + fn on_mouse_down_weird( + mut self, + button: MouseButton, + listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, + ) -> Self { + self.interactivity().mouse_down_listeners.push(Box::new( + move |event, bounds, phase, cx| { + dbg!("HEREEEE"); + + let contains = dbg!(dbg!(&bounds.bounds).contains_point(dbg!(&event.position))) + && dbg!(cx.was_top_layer(&event.position, &bounds.stacking_order)); + dbg!(contains); + + if phase == DispatchPhase::Bubble + && event.button == button + && bounds.visibly_contains(&event.position, cx) + { + dbg!("HEREEEE2"); + + (listener)(event, cx) + } + }, + )); + self + } + fn on_any_mouse_down( mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, @@ -767,7 +793,7 @@ pub struct Interactivity { pub tooltip_builder: Option, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct InteractiveBounds { pub bounds: Bounds, pub stacking_order: StackingOrder, diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 03e98e831d8013608b1483a08c95122978f1ba73..211d74ea3a8177d07242e096a21d77f463bb4d08 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -638,7 +638,7 @@ impl TerminalElement { let connection = self.terminal.clone(); let mut this = self - .on_mouse_down(MouseButton::Left, { + .on_mouse_down_weird(MouseButton::Left, { let connection = connection.clone(); let focus = focus.clone(); move |e, cx| { @@ -814,6 +814,7 @@ impl Element for TerminalElement { state: &mut Self::State, cx: &mut WindowContext<'_>, ) { + dbg!(bounds); let mut layout = self.compute_layout(bounds, cx); let theme = cx.theme(); @@ -832,54 +833,51 @@ impl Element for TerminalElement { let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx); let interactivity = mem::take(&mut this.interactivity); - cx.with_z_index(0, |cx| { - interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { - this.register_key_listeners(cx); + interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { + this.register_key_listeners(cx); - for rect in &layout.rects { - rect.paint(origin, &layout, cx); - } + for rect in &layout.rects { + rect.paint(origin, &layout, cx); + } - cx.with_z_index(1, |cx| { - for (relative_highlighted_range, color) in - layout.relative_highlighted_ranges.iter() + cx.with_z_index(1, |cx| { + for (relative_highlighted_range, color) in layout.relative_highlighted_ranges.iter() + { + if let Some((start_y, highlighted_range_lines)) = + to_highlighted_range_lines(relative_highlighted_range, &layout, origin) { - if let Some((start_y, highlighted_range_lines)) = - to_highlighted_range_lines(relative_highlighted_range, &layout, origin) - { - let hr = HighlightedRange { - start_y, //Need to change this - line_height: layout.size.line_height, - lines: highlighted_range_lines, - color: color.clone(), - //Copied from editor. TODO: move to theme or something - corner_radius: 0.15 * layout.size.line_height, - }; - hr.paint(bounds, cx); - } - } - }); - - cx.with_z_index(2, |cx| { - for cell in &layout.cells { - cell.paint(origin, &layout, bounds, cx); + let hr = HighlightedRange { + start_y, //Need to change this + line_height: layout.size.line_height, + lines: highlighted_range_lines, + color: color.clone(), + //Copied from editor. TODO: move to theme or something + corner_radius: 0.15 * layout.size.line_height, + }; + hr.paint(bounds, cx); } - }); - - if this.cursor_visible { - cx.with_z_index(3, |cx| { - if let Some(cursor) = &layout.cursor { - cursor.paint(origin, cx); - } - }); } + }); - if let Some(element) = layout.hyperlink_tooltip.take() { - let width: AvailableSpace = bounds.size.width.into(); - let height: AvailableSpace = bounds.size.height.into(); - element.draw(origin, Size { width, height }, cx) + cx.with_z_index(2, |cx| { + for cell in &layout.cells { + cell.paint(origin, &layout, bounds, cx); } }); + + if this.cursor_visible { + cx.with_z_index(3, |cx| { + if let Some(cursor) = &layout.cursor { + cursor.paint(origin, cx); + } + }); + } + + if let Some(element) = layout.hyperlink_tooltip.take() { + let width: AvailableSpace = bounds.size.width.into(); + let height: AvailableSpace = bounds.size.height.into(); + element.draw(origin, Size { width, height }, cx) + } }); } } diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index e9e4cd516762d240c191e54e374ccdec9acc05f2..17d4829e7cfea1958f4212bb07458508cc114ed8 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -266,18 +266,18 @@ impl TerminalView { .detach(); let focus = cx.focus_handle(); - let focus_in = cx.on_focus_in(&focus, |this, cx| { - this.has_new_content = false; - this.terminal.read(cx).focus_in(); - this.blink_cursors(this.blink_epoch, cx); - cx.notify(); - }); - let focus_out = cx.on_focus_out(&focus, |this, cx| { - this.terminal.update(cx, |terminal, _| { - terminal.focus_out(); - }); - cx.notify(); - }); + // let focus_in = cx.on_focus_in(&focus, |this, cx| { + // this.has_new_content = false; + // this.terminal.read(cx).focus_in(); + // this.blink_cursors(this.blink_epoch, cx); + // cx.notify(); + // }); + // let focus_out = cx.on_focus_out(&focus, |this, cx| { + // this.terminal.update(cx, |terminal, _| { + // terminal.focus_out(); + // }); + // cx.notify(); + // }); Self { terminal, @@ -291,7 +291,7 @@ impl TerminalView { blink_epoch: 0, can_navigate_to_selected_word: false, workspace_id, - _subscriptions: vec![focus_in, focus_out], + _subscriptions: vec![/*focus_in, focus_out*/], } } From b29cea287ba69a2841d0fd9e6c8ef5264a1b5c88 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 12:12:28 +0100 Subject: [PATCH 42/90] WIP --- crates/assistant2/src/assistant_panel.rs | 6 +-- crates/assistant2/src/codegen.rs | 53 ++++++++++++------------ crates/assistant2/src/prompts.rs | 4 +- crates/search2/src/buffer_search.rs | 12 ++++-- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index bcf85a6948eb8cba330a0a54bae24440b7462fff..31264186bd92dc2f378c0b6eadbdc071799a004d 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2835,7 +2835,7 @@ impl InlineAssistant { ) })?; - if answer.next().await == Some(0) { + if answer.await? == 0 { this.update(&mut cx, |this, _| { this.semantic_permissioned = Some(true); })?; @@ -2875,7 +2875,7 @@ impl InlineAssistant { // This has to be updated to accomodate for semantic_permissions if semantic_permissioned.await.unwrap_or(false) { semantic_index - .update(&mut cx, |index, cx| index.index_project(project, cx)) + .update(&mut cx, |index, cx| index.index_project(project, cx))? .await } else { Err(anyhow!("project is not permissioned for semantic indexing")) @@ -3453,7 +3453,7 @@ fn report_assistant_event( .default_open_ai_model .clone(); - let telemetry_settings = TelemetrySettings::get_global(cx); + let telemetry_settings = TelemetrySettings::get_global(cx).clone(); telemetry.report_assistant_event( telemetry_settings, diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs index 9696c629ac910010d193057adcdb1db3f2070935..98b43719f3d7f58b1479d9612d843a09ed68c3b1 100644 --- a/crates/assistant2/src/codegen.rs +++ b/crates/assistant2/src/codegen.rs @@ -109,13 +109,13 @@ impl Codegen { .unwrap_or_else(|| snapshot.indent_size_for_line(selection_start.row)); let response = self.provider.complete(prompt); - self.generation = cx.spawn_weak(|this, mut cx| { + self.generation = cx.spawn(|this, mut cx| { async move { let generate = async { let mut edit_start = range.start.to_offset(&snapshot); let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1); - let diff = cx.background().spawn(async move { + let diff = cx.background_executor().spawn(async move { let chunks = strip_invalid_spans_from_codeblock(response.await?); futures::pin_mut!(chunks); let mut diff = StreamingDiff::new(selected_text.to_string()); @@ -181,7 +181,7 @@ impl Codegen { }); while let Some(hunks) = hunks_rx.next().await { - let this = if let Some(this) = this.upgrade(&cx) { + let this = if let Some(this) = this.upgrade() { this } else { break; @@ -251,17 +251,16 @@ impl Codegen { }; let result = generate.await; - if let Some(this) = this.upgrade(&cx) { - this.update(&mut cx, |this, cx| { - this.last_equal_ranges.clear(); - this.idle = true; - if let Err(error) = result { - this.error = Some(error); - } - cx.emit(Event::Finished); - cx.notify(); - }); - } + this.update(&mut cx, |this, cx| { + this.last_equal_ranges.clear(); + this.idle = true; + if let Err(error) = result { + this.error = Some(error); + } + cx.emit(Event::Finished); + cx.notify(); + }) + .ok(); } }); self.error.take(); @@ -370,7 +369,7 @@ mod tests { use super::*; use ai::test::FakeCompletionProvider; use futures::stream::{self}; - use gpui::TestAppContext; + use gpui::{Context, TestAppContext}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use rand::prelude::*; @@ -390,7 +389,7 @@ mod tests { #[gpui::test(iterations = 10)] async fn test_transform_autoindent(cx: &mut TestAppContext, mut rng: StdRng) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -402,14 +401,14 @@ mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let range = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Transform { range }, @@ -459,7 +458,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = indoc! {" @@ -468,14 +467,14 @@ mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let position = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 6)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, @@ -524,7 +523,7 @@ mod tests { cx: &mut TestAppContext, mut rng: StdRng, ) { - cx.set_global(cx.read(SettingsStore::test)); + cx.set_global(cx.update(SettingsStore::test)); cx.update(language_settings::init); let text = concat!( @@ -533,14 +532,14 @@ mod tests { "}\n" // ); let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); - let buffer = cx.add_model(|cx| MultiBuffer::singleton(buffer, cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let position = buffer.read_with(cx, |buffer, cx| { let snapshot = buffer.snapshot(cx); snapshot.anchor_before(Point::new(1, 2)) }); let provider = Arc::new(FakeCompletionProvider::new()); - let codegen = cx.add_model(|cx| { + let codegen = cx.build_model(|cx| { Codegen::new( buffer.clone(), CodegenKind::Generate { position }, diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs index b678c6fe3b402129692b02f0094841cafe92ffca..ac3b175e644aeac15cdb3d18c5112c6996d3cf7f 100644 --- a/crates/assistant2/src/prompts.rs +++ b/crates/assistant2/src/prompts.rs @@ -176,7 +176,7 @@ pub(crate) mod tests { use super::*; use std::sync::Arc; - use gpui::AppContext; + use gpui::{AppContext, Context}; use indoc::indoc; use language::{language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, Point}; use settings::SettingsStore; @@ -253,7 +253,7 @@ pub(crate) mod tests { } "}; let buffer = - cx.add_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); + cx.build_model(|cx| Buffer::new(0, 0, text).with_language(Arc::new(rust_lang()), cx)); let snapshot = buffer.read(cx).snapshot(); assert_eq!( diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index b3d600611327f67802db738f57411b322593420d..b9fa36ef34e4b7413043a8d1c76357c55953270a 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -10,9 +10,9 @@ use collections::HashMap; use editor::{Editor, EditorMode}; use futures::channel::oneshot; use gpui::{ - actions, div, red, Action, AppContext, Div, EventEmitter, InteractiveElement as _, IntoElement, - ParentElement as _, Render, Styled, Subscription, Task, View, ViewContext, VisualContext as _, - WeakView, WindowContext, + actions, div, red, Action, AppContext, Div, EventEmitter, FocusableView, + InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled, Subscription, Task, + View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use project::search::SearchQuery; use serde::Deserialize; @@ -251,6 +251,12 @@ impl Render for BufferSearchBar { } } +impl FocusableView for BufferSearchBar { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.query_editor.focus_handle(cx) + } +} + impl ToolbarItemView for BufferSearchBar { fn set_active_pane_item( &mut self, From 1de02cf6e5f34383baf1feaedda28251e1d95426 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 12:51:25 +0100 Subject: [PATCH 43/90] Start wiring up assistant2 --- Cargo.lock | 2 + crates/assistant2/src/assistant_panel.rs | 229 ++++++++++++----------- crates/assistant2/src/codegen.rs | 8 +- crates/assistant2/src/prompts.rs | 3 +- crates/zed2/Cargo.toml | 4 +- crates/zed2/src/main.rs | 12 +- 6 files changed, 137 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3808d17f4f908eaabea911d39513261723ff8343..945ff79edd63f913f01700e3b07aefd912337786 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11881,6 +11881,7 @@ dependencies = [ "activity_indicator2", "ai2", "anyhow", + "assistant2", "async-compression", "async-recursion 0.3.2", "async-tar", @@ -11939,6 +11940,7 @@ dependencies = [ "rust-embed", "schemars", "search2", + "semantic_index2", "serde", "serde_derive", "serde_json", diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 31264186bd92dc2f378c0b6eadbdc071799a004d..c9a9d0d2519536f921d667e1f4e5434cd04b1973 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -22,16 +22,18 @@ use editor::{ BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint, }, scroll::autoscroll::{Autoscroll, AutoscrollStrategy}, - Anchor, Editor, EditorEvent, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint, + Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MoveDown, MoveUp, MultiBufferSnapshot, + ToOffset, ToPoint, }; use fs::Fs; use futures::StreamExt; use gpui::{ - actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, - ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, - HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels, - PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext, + actions, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, + AsyncWindowContext, ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, + FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, + ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, + StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, + View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; @@ -49,6 +51,7 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; +use theme::{ActiveTheme, ThemeSettings}; use ui::{ h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, IconElement, Label, Selectable, Tooltip, @@ -77,7 +80,7 @@ actions!( pub fn init(cx: &mut AppContext) { AssistantSettings::register(cx); cx.observe_new_views( - |workspace: &mut Workspace, cx: &mut ViewContext| { + |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace .register_action(|workspace, _: &ToggleFocus, cx| { workspace.toggle_panel_focus::(cx); @@ -122,7 +125,7 @@ impl AssistantPanel { pub fn load( workspace: WeakView, - mut cx: AsyncWindowContext, + cx: AsyncWindowContext, ) -> Task>> { cx.spawn(|mut cx| async move { let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; @@ -540,7 +543,7 @@ impl AssistantPanel { if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) { if let hash_map::Entry::Occupied(mut entry) = self .pending_inline_assist_ids_by_editor - .entry(pending_assist.editor) + .entry(pending_assist.editor.clone()) { entry.get_mut().retain(|id| *id != assist_id); if entry.get().is_empty() { @@ -747,7 +750,7 @@ impl AssistantPanel { temperature, }); - codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx)); + codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?; anyhow::Ok(()) }) .detach(); @@ -779,7 +782,7 @@ impl AssistantPanel { } else { editor.highlight_background::( background_ranges, - |theme| gpui::red(), // todo!("use the appropriate color") + |theme| theme.editor_active_line_background, // todo!("use the appropriate color") cx, ); } @@ -1240,7 +1243,7 @@ impl Panel for AssistantPanel { } } - fn icon(&self, cx: &WindowContext) -> Option { + fn icon(&self, _cx: &WindowContext) -> Option { Some(Icon::Ai) } @@ -1862,7 +1865,7 @@ impl Conversation { .text .push_str(&text); cx.emit(ConversationEvent::SummaryChanged); - }); + })?; } this.update(&mut cx, |this, cx| { @@ -1870,7 +1873,7 @@ impl Conversation { summary.done = true; cx.emit(ConversationEvent::SummaryChanged); } - }); + })?; anyhow::Ok(()) } @@ -2249,7 +2252,7 @@ impl ConversationEditor { style: BlockStyle::Sticky, render: Arc::new({ let conversation = self.conversation.clone(); - move |cx| { + move |_cx| { let message_id = message.id; let sender = ButtonLike::new("role") .child(match message.role { @@ -2277,16 +2280,18 @@ impl ConversationEditor { .border_color(gpui::red()) .child(sender) .child(Label::new(message.sent_at.format("%I:%M%P").to_string())) - .children(if let MessageStatus::Error(error) = &message.status { - Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text(error, cx)) - .child(IconElement::new(Icon::XCircle)), - ) - } else { - None - }) + .children( + if let MessageStatus::Error(error) = message.status.clone() { + Some( + div() + .id("error") + .tooltip(move |cx| Tooltip::text(&error, cx)) + .child(IconElement::new(Icon::XCircle)), + ) + } else { + None + }, + ) .into_any_element() } }), @@ -2602,10 +2607,11 @@ impl Render for InlineAssistant { None }) .children(if let Some(error) = self.codegen.read(cx).error() { + let error_message = SharedString::from(error.to_string()); Some( div() .id("error") - .tooltip(|cx| Tooltip::text(error.to_string(), cx)) + .tooltip(move |cx| Tooltip::text(error_message.clone(), cx)) .child(IconElement::new(Icon::XCircle).color(Color::Error)), ) } else { @@ -2615,7 +2621,7 @@ impl Render for InlineAssistant { .child( div() .ml(measurements.anchor_x - measurements.gutter_width) - .child(self.prompt_editor.clone()), + .child(self.render_prompt_editor(cx)), ) .children(if self.retrieve_context { self.retrieve_context_status(cx) @@ -2752,24 +2758,14 @@ impl InlineAssistant { fn handle_codegen_changed(&mut self, _: Model, cx: &mut ViewContext) { let is_read_only = !self.codegen.read(cx).idle(); - self.prompt_editor.update(cx, |editor, cx| { + self.prompt_editor.update(cx, |editor, _cx| { let was_read_only = editor.read_only(); if was_read_only != is_read_only { if is_read_only { editor.set_read_only(true); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); } else { self.confirmed = false; editor.set_read_only(false); - editor.set_field_editor_style( - Some(Arc::new(|theme| theme.assistant.inline.editor.clone())), - cx, - ); } } }); @@ -2787,15 +2783,8 @@ impl InlineAssistant { report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx); let prompt = self.prompt_editor.read(cx).text(cx); - self.prompt_editor.update(cx, |editor, cx| { - editor.set_read_only(true); - editor.set_field_editor_style( - Some(Arc::new(|theme| { - theme.assistant.inline.disabled_editor.clone() - })), - cx, - ); - }); + self.prompt_editor + .update(cx, |editor, _cx| editor.set_read_only(true)); cx.emit(InlineAssistantEvent::Confirmed { prompt, include_conversation: self.include_conversation, @@ -2827,7 +2816,7 @@ impl InlineAssistant { cx.spawn(|this, mut cx| async move { // If Necessary prompt user if !semantic_permissioned.await.unwrap_or(false) { - let mut answer = this.update(&mut cx, |_, cx| { + let answer = this.update(&mut cx, |_, cx| { cx.prompt( PromptLevel::Info, prompt_text.as_str(), @@ -2888,71 +2877,68 @@ impl InlineAssistant { } fn retrieve_context_status(&self, cx: &mut ViewContext) -> Option { - enum ContextStatusIcon {} - let Some(project) = self.project.upgrade() else { return None; }; - if let Some(semantic_index) = SemanticIndex::global(cx) { - let status = semantic_index.update(cx, |index, _| index.status(&project)); - match status { - SemanticIndexStatus::NotAuthenticated {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), + let semantic_index = SemanticIndex::global(cx)?; + let status = semantic_index.update(cx, |index, _| index.status(&project)); + match status { + SemanticIndexStatus::NotAuthenticated {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() + ), - SemanticIndexStatus::NotIndexed {} => Some( - div() - .id("error") - .tooltip(|cx| Tooltip::text("Not Indexed", cx)) - .child(IconElement::new(Icon::XCircle)) - .into_any_element() - ), - - SemanticIndexStatus::Indexing { - remaining_files, - rate_limit_expiry, - } => { - let mut status_text = if remaining_files == 0 { - "Indexing...".to_string() - } else { - format!("Remaining files to index: {remaining_files}") - }; + SemanticIndexStatus::NotIndexed {} => Some( + div() + .id("error") + .tooltip(|cx| Tooltip::text("Not Indexed", cx)) + .child(IconElement::new(Icon::XCircle)) + .into_any_element() + ), - if let Some(rate_limit_expiry) = rate_limit_expiry { - let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); - if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { - write!( - status_text, - " (rate limit expires in {}s)", - remaining_seconds.as_secs() - ) - .unwrap(); - } + SemanticIndexStatus::Indexing { + remaining_files, + rate_limit_expiry, + } => { + let mut status_text = if remaining_files == 0 { + "Indexing...".to_string() + } else { + format!("Remaining files to index: {remaining_files}") + }; + + if let Some(rate_limit_expiry) = rate_limit_expiry { + let remaining_seconds = rate_limit_expiry.duration_since(Instant::now()); + if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 { + write!( + status_text, + " (rate limit expires in {}s)", + remaining_seconds.as_secs() + ) + .unwrap(); } - Some( - div() - .id("update") - .tooltip(|cx| Tooltip::text(status_text, cx)) - .child(IconElement::new(Icon::Update).color(Color::Info)) - .into_any_element() - ) } - SemanticIndexStatus::Indexed {} => Some( + let status_text = SharedString::from(status_text); + Some( div() - .id("check") - .tooltip(|cx| Tooltip::text("Index up to date", cx)) - .child(IconElement::new(Icon::Check).color(Color::Success)) + .id("update") + .tooltip(move |cx| Tooltip::text(status_text.clone(), cx)) + .child(IconElement::new(Icon::Update).color(Color::Info)) .into_any_element() - ), + ) } - } else { - None + + SemanticIndexStatus::Indexed {} => Some( + div() + .id("check") + .tooltip(|cx| Tooltip::text("Index up to date", cx)) + .child(IconElement::new(Icon::Check).color(Color::Success)) + .into_any_element() + ), } } @@ -3004,6 +2990,35 @@ impl InlineAssistant { }); }); } + + fn render_prompt_editor(&self, cx: &mut ViewContext) -> impl IntoElement { + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: if self.prompt_editor.read(cx).read_only() { + cx.theme().colors().text_disabled + } else { + cx.theme().colors().text + }, + font_family: settings.ui_font.family.clone(), + font_features: settings.ui_font.features, + font_size: rems(0.875).into(), + font_weight: FontWeight::NORMAL, + font_style: FontStyle::Normal, + line_height: relative(1.).into(), + background_color: None, + underline: None, + white_space: WhiteSpace::Normal, + }; + EditorElement::new( + &self.prompt_editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + ..Default::default() + }, + ) + } } // This wouldn't need to exist if we could pass parameters when rendering child views. @@ -3052,7 +3067,8 @@ mod tests { #[gpui::test] fn test_inserting_and_removing_messages(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); @@ -3183,7 +3199,8 @@ mod tests { #[gpui::test] fn test_message_splitting(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); @@ -3282,7 +3299,8 @@ mod tests { #[gpui::test] fn test_messages_for_offsets(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); @@ -3366,7 +3384,8 @@ mod tests { #[gpui::test] fn test_serialization(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init(cx); let registry = Arc::new(LanguageRegistry::test()); let completion_provider = Arc::new(FakeCompletionProvider::new()); diff --git a/crates/assistant2/src/codegen.rs b/crates/assistant2/src/codegen.rs index 98b43719f3d7f58b1479d9612d843a09ed68c3b1..2f7b2f03785e65a8ad3dd4a672b2c7fd820ec0bc 100644 --- a/crates/assistant2/src/codegen.rs +++ b/crates/assistant2/src/codegen.rs @@ -181,12 +181,6 @@ impl Codegen { }); while let Some(hunks) = hunks_rx.next().await { - let this = if let Some(this) = this.upgrade() { - this - } else { - break; - }; - this.update(&mut cx, |this, cx| { this.last_equal_ranges.clear(); @@ -243,7 +237,7 @@ impl Codegen { } cx.notify(); - }); + })?; } diff.await?; diff --git a/crates/assistant2/src/prompts.rs b/crates/assistant2/src/prompts.rs index ac3b175e644aeac15cdb3d18c5112c6996d3cf7f..06881ad9c27935bae19ec9a3eaaed2e4f133d9fc 100644 --- a/crates/assistant2/src/prompts.rs +++ b/crates/assistant2/src/prompts.rs @@ -227,7 +227,8 @@ pub(crate) mod tests { #[gpui::test] fn test_outline_for_prompt(cx: &mut AppContext) { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); language_settings::init(cx); let text = indoc! {" struct X { diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index ee9416e234188400164f2ebd1f713bc145364862..9fef3232402b7f04e342313b28529f1b38d7c376 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -49,7 +49,7 @@ lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } # language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } -# assistant = { path = "../assistant" } +assistant = { package = "assistant2", path = "../assistant2" } outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } @@ -68,7 +68,7 @@ terminal_view = { package = "terminal_view2", path = "../terminal_view2" } theme = { package = "theme2", path = "../theme2" } theme_selector = { package = "theme_selector2", path = "../theme_selector2" } util = { path = "../util" } -# semantic_index = { path = "../semantic_index" } +semantic_index = { package = "semantic_index2", path = "../semantic_index2" } # vim = { path = "../vim" } workspace = { package = "workspace2", path = "../workspace2" } welcome = { package = "welcome2", path = "../welcome2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 6ca5d1a805b53ab1dabd43591b7353c0231aa365..eafd4924c726d1fd92273d26199dbbf2411ecdd9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -161,11 +161,11 @@ fn main() { node_runtime.clone(), cx, ); - // assistant::init(cx); + assistant::init(cx); // component_test::init(cx); - // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) - // .detach(); + cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) + .detach(); watch_file_types(fs.clone(), cx); languages.set_theme(cx.theme().clone()); @@ -186,10 +186,10 @@ fn main() { .report_app_event(telemetry_settings, event_operation); let app_state = Arc::new(AppState { - languages, + languages: languages.clone(), client: client.clone(), user_store: user_store.clone(), - fs, + fs: fs.clone(), build_window_options, workspace_store, node_runtime, @@ -210,7 +210,7 @@ fn main() { channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); search::init(cx); - // semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); + semantic_index::init(fs.clone(), http.clone(), languages.clone(), cx); // vim::init(cx); terminal_view::init(cx); From 3f9fe58c48a9722a3dd8ccf62081db2dbbc404a1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 13:00:57 +0100 Subject: [PATCH 44/90] Signed out state is looking good --- Cargo.lock | 1 + crates/copilot2/Cargo.toml | 1 + crates/copilot2/src/sign_in.rs | 278 +++++++++++++++++------------- crates/ui2/src/components/icon.rs | 3 + 4 files changed, 164 insertions(+), 119 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e689388a132306158b634fd1776476233d7a03f3..6b123f9061d4ba7b15d606e7896c65c22a3c8245 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2121,6 +2121,7 @@ dependencies = [ "settings2", "smol", "theme2", + "ui2", "util", ] diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index 9a9243b32eecb766451a3f7f89940227fe6059fa..b04a7d1246ebfbcbb03dbdf179d2633934cecd98 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -28,6 +28,7 @@ theme = { package = "theme2", path = "../theme2" } lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } +ui = { package = "ui2", path = "../ui2" } async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } async-tar = "0.4.2" anyhow.workspace = true diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index 7973d935af1be1c455bb2b90a0ed8dcf2c1b3566..e39deeff83b168d653546c237eba8464f1516c1e 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -11,11 +11,15 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; -use crate::{Copilot, Status}; +use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - px, size, AppContext, Bounds, Div, GlobalPixels, Point, Render, ViewContext, VisualContext, - WindowBounds, WindowHandle, WindowKind, WindowOptions, + div, px, red, size, AnyElement, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, + InteractiveElement, IntoElement, MouseButton, ParentElement, Point, Render, Stateful, + StatefulInteractiveElement, Styled, ViewContext, VisualContext, WindowBounds, WindowHandle, + WindowKind, WindowOptions, }; +use theme::ActiveTheme; +use ui::{h_stack, v_stack, Button, Clickable, Icon, IconElement, Label}; pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { @@ -72,13 +76,14 @@ fn create_copilot_auth_window( center: true, focus: true, show: true, - kind: WindowKind::Normal, + kind: WindowKind::PopUp, is_movable: true, display_id: None, }; - cx.open_window(window_options, |cx| { + let window = cx.open_window(window_options, |cx| { cx.build_view(|_| CopilotCodeVerification::new(status.clone())) - }) + }); + window } pub struct CopilotCodeVerification { @@ -99,119 +104,138 @@ impl CopilotCodeVerification { cx.notify(); } - // fn render_device_code( - // data: &PromptUserDeviceFlow, - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> impl IntoAnyElement { - // let copied = cx - // .read_from_clipboard() - // .map(|item| item.text() == &data.user_code) - // .unwrap_or(false); - - // let device_code_style = &style.auth.prompting.device_code; - - // MouseEventHandler::new::(0, cx, |state, _cx| { - // Flex::row() - // .with_child( - // Label::new(data.user_code.clone(), device_code_style.text.clone()) - // .aligned() - // .contained() - // .with_style(device_code_style.left_container) - // .constrained() - // .with_width(device_code_style.left), - // ) - // .with_child( - // Label::new( - // if copied { "Copied!" } else { "Copy" }, - // device_code_style.cta.style_for(state).text.clone(), - // ) - // .aligned() - // .contained() - // .with_style(*device_code_style.right_container.style_for(state)) - // .constrained() - // .with_width(device_code_style.right), - // ) - // .contained() - // .with_style(device_code_style.cta.style_for(state).container) - // }) - // .on_click(gpui::platform::MouseButton::Left, { - // let user_code = data.user_code.clone(); - // move |_, _, cx| { - // cx.platform() - // .write_to_clipboard(ClipboardItem::new(user_code.clone())); - // cx.notify(); - // } - // }) - // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) - // } + fn render_device_code( + data: &PromptUserDeviceFlow, + cx: &mut ViewContext, + ) -> impl IntoElement { + let copied = cx + .read_from_clipboard() + .map(|item| item.text() == &data.user_code) + .unwrap_or(false); + h_stack() + .cursor_pointer() + .justify_between() + .on_mouse_down(gpui::MouseButton::Left, { + let user_code = data.user_code.clone(); + move |_, cx| { + dbg!("Copied"); + cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); + cx.notify(); + } + }) + .child(Label::new(data.user_code.clone())) + .child(Label::new(if copied { "Copied!" } else { "Copy" })) - // fn render_prompting_modal( - // connect_clicked: bool, - // data: &PromptUserDeviceFlow, - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum ConnectButton {} + // MouseEventHandler::new::(0, cx, |state, _cx| { + // Flex::row() + // .with_child( + // Label::new(data.user_code.clone(), device_code_style.text.clone()) + // .aligned() + // .contained() + // .with_style(device_code_style.left_container) + // .constrained() + // .with_width(device_code_style.left), + // ) + // .with_child( + // Label::new( + // if copied { "Copied!" } else { "Copy" }, + // device_code_style.cta.style_for(state).text.clone(), + // ) + // .aligned() + // .contained() + // .with_style(*device_code_style.right_container.style_for(state)) + // .constrained() + // .with_width(device_code_style.right), + // ) + // .contained() + // .with_style(device_code_style.cta.style_for(state).container) + // }) + // .on_click(gpui::platform::MouseButton::Left, { + // + // move |_, _, cx| { + // + // } + // }) + // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) + } - // Flex::column() - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Enable Copilot by connecting", - // style.auth.prompting.subheading.text.clone(), - // ) - // .aligned(), - // Label::new( - // "your existing license.", - // style.auth.prompting.subheading.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(style.auth.prompting.subheading.container), - // ) - // .with_child(Self::render_device_code(data, &style, cx)) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Paste this code into GitHub after", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // Label::new( - // "clicking the button below.", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(style.auth.prompting.hint.container.clone()), - // ) - // .with_child(theme::ui::cta_button::( - // if connect_clicked { - // "Waiting for connection..." - // } else { - // "Connect to GitHub" - // }, - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // { - // let verification_uri = data.verification_uri.clone(); - // move |_, verification, cx| { - // cx.platform().open_url(&verification_uri); - // verification.connect_clicked = true; - // } - // }, - // )) - // .align_children_center() - // .into_any() - // } + fn render_prompting_modal( + connect_clicked: bool, + data: &PromptUserDeviceFlow, + cx: &mut ViewContext, + ) -> impl Element { + let connect_button_label = if connect_clicked { + "Waiting for connection..." + } else { + "Connect to Github" + }; + v_stack() + .child( + v_stack() + .flex_1() + .w_full() + .items_center() + .justify_between() + .children([ + h_stack() + .items_center() + .child(Label::new("Enable Copilot by connecting")), + h_stack() + .items_center() + .child(Label::new("your existing license")), + ]), + ) + .child(Self::render_device_code(data, cx)) + .child(Label::new("Paste this code into GitHub after").size(ui::LabelSize::Small)) + .child(Label::new("clicking the button below.").size(ui::LabelSize::Small)) + .child( + Button::new("connect-button", connect_button_label).on_click({ + let verification_uri = data.verification_uri.clone(); + cx.listener(move |this, _, cx| { + cx.open_url(&verification_uri); + this.connect_clicked = true; + }) + }), + ) + // Flex::column() + // .with_child(Self::render_device_code(data, &style, cx)) + // .with_child( + // Flex::column() + // .with_children([ + // Label::new( + // "Paste this code into GitHub after", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // Label::new( + // "clicking the button below.", + // style.auth.prompting.hint.text.clone(), + // ) + // .aligned(), + // ]) + // .align_children_center() + // .contained() + // .with_style(style.auth.prompting.hint.container.clone()), + // ) + // .with_child(theme::ui::cta_button::( + // if connect_clicked { + // "Waiting for connection..." + // } else { + // "Connect to GitHub" + // }, + // style.auth.content_width, + // &style.auth.cta_button, + // cx, + // { + // let verification_uri = data.verification_uri.clone(); + // move |_, verification, cx| { + // cx.platform().open_url(&verification_uri); + // verification.connect_clicked = true; + // } + // }, + // )) + // .align_children_center() + } // fn render_enabled_modal( // style: &theme::Copilot, @@ -316,10 +340,26 @@ impl CopilotCodeVerification { } impl Render for CopilotCodeVerification { - type Element = Div; + type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + let prompt = match &self.status { + Status::SigningIn { prompt } => prompt.as_ref(), + _ => None, + }; + div() + .id("copilot code verification") + .flex() + .flex_col() + .size_full() + .items_center() + .p_10() + .bg(cx.theme().colors().element_background) + .child(ui::Label::new("Connect Copilot to Zed")) + .child(IconElement::new(Icon::ZedXCopilot)) + .children( + prompt.map(|data| Self::render_prompting_modal(self.connect_clicked, data, cx)), + ) } } diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a993a54e15463d14cbdf8c14325aec96480204e6..e9a13bd009b0afd6b23e89e773f81d93684d8989 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -81,6 +81,7 @@ pub enum Icon { Shift, Option, Return, + ZedXCopilot, } impl Icon { @@ -109,6 +110,7 @@ impl Icon { Icon::Close => "icons/x.svg", Icon::Collab => "icons/user_group_16.svg", Icon::Copilot => "icons/copilot.svg", + Icon::CopilotInit => "icons/copilot_init.svg", Icon::CopilotError => "icons/copilot_error.svg", Icon::CopilotDisabled => "icons/copilot_disabled.svg", @@ -155,6 +157,7 @@ impl Icon { Icon::Shift => "icons/shift.svg", Icon::Option => "icons/option.svg", Icon::Return => "icons/return.svg", + Icon::ZedXCopilot => "icons/zed_x_copilot.svg", } } } From 7998e8281c384eb58a6099df0111699856b84079 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:14:18 +0100 Subject: [PATCH 45/90] Barebones Copilot prompt Filter out sign in/sign out when user is signed in/not signed in --- crates/copilot2/src/copilot2.rs | 14 ++++-- crates/copilot2/src/sign_in.rs | 87 ++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 32 deletions(-) diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index d23d25119b152be1c232304d2992af81af3ab9e4..a829f68f41b266dc9a5896a2fffe2a926ac3c805 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -60,8 +60,8 @@ pub fn init( TypeId::of::(), TypeId::of::(), ]; - let copilot_auth_action_types = [TypeId::of::(), TypeId::of::()]; - + let copilot_auth_action_types = [TypeId::of::()]; + let copilot_no_auth_action_types = [TypeId::of::()]; let status = handle.read(cx).status(); let filter = cx.default_global::(); @@ -69,8 +69,14 @@ pub fn init( Status::Disabled => { filter.hidden_action_types.extend(copilot_action_types); filter.hidden_action_types.extend(copilot_auth_action_types); + filter + .hidden_action_types + .extend(copilot_no_auth_action_types); } Status::Authorized => { + filter + .hidden_action_types + .extend(copilot_no_auth_action_types); for type_id in copilot_action_types .iter() .chain(&copilot_auth_action_types) @@ -80,7 +86,8 @@ pub fn init( } _ => { filter.hidden_action_types.extend(copilot_action_types); - for type_id in &copilot_auth_action_types { + filter.hidden_action_types.extend(copilot_auth_action_types); + for type_id in &copilot_no_auth_action_types { filter.hidden_action_types.remove(type_id); } } @@ -97,6 +104,7 @@ pub fn init( } }); cx.on_action(|_: &SignOut, cx| { + dbg!("Signing out"); if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index e39deeff83b168d653546c237eba8464f1516c1e..dd107532fa1f55cf2baa9d5d1305c53f37814022 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -13,13 +13,12 @@ const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ - div, px, red, size, AnyElement, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, - InteractiveElement, IntoElement, MouseButton, ParentElement, Point, Render, Stateful, - StatefulInteractiveElement, Styled, ViewContext, VisualContext, WindowBounds, WindowHandle, - WindowKind, WindowOptions, + div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement, + IntoElement, ParentElement, Point, Render, Stateful, Styled, ViewContext, VisualContext, + WindowBounds, WindowHandle, WindowKind, WindowOptions, }; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Button, Clickable, Icon, IconElement, Label}; +use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label}; pub fn init(cx: &mut AppContext) { if let Some(copilot) = Copilot::global(cx) { @@ -56,7 +55,9 @@ pub fn init(cx: &mut AppContext) { } _ => { if let Some(code_verification) = verification_window.take() { - code_verification.update(cx, |_, cx| cx.remove_window()); + code_verification + .update(cx, |_, cx| cx.remove_window()) + .ok(); } } } @@ -118,12 +119,12 @@ impl CopilotCodeVerification { .on_mouse_down(gpui::MouseButton::Left, { let user_code = data.user_code.clone(); move |_, cx| { - dbg!("Copied"); cx.write_to_clipboard(ClipboardItem::new(user_code.clone())); cx.notify(); } }) .child(Label::new(data.user_code.clone())) + .child(div()) .child(Label::new(if copied { "Copied!" } else { "Copy" })) // MouseEventHandler::new::(0, cx, |state, _cx| { @@ -170,24 +171,18 @@ impl CopilotCodeVerification { "Connect to Github" }; v_stack() + .flex_1() + .items_center() + .justify_between() + .w_full() + .child(Label::new( + "Enable Copilot by connecting your existing license", + )) + .child(Self::render_device_code(data, cx)) .child( - v_stack() - .flex_1() - .w_full() - .items_center() - .justify_between() - .children([ - h_stack() - .items_center() - .child(Label::new("Enable Copilot by connecting")), - h_stack() - .items_center() - .child(Label::new("your existing license")), - ]), + Label::new("Paste this code into GitHub after clicking the button below.") + .size(ui::LabelSize::Small), ) - .child(Self::render_device_code(data, cx)) - .child(Label::new("Paste this code into GitHub after").size(ui::LabelSize::Small)) - .child(Label::new("clicking the button below.").size(ui::LabelSize::Small)) .child( Button::new("connect-button", connect_button_label).on_click({ let verification_uri = data.verification_uri.clone(); @@ -236,6 +231,17 @@ impl CopilotCodeVerification { // )) // .align_children_center() } + fn render_enabled_modal() -> impl Element { + v_stack() + .child(Label::new("Copilot Enabled!")) + .child(Label::new( + "You can update your settings or sign out from the Copilot menu in the status bar.", + )) + .child( + Button::new("copilot-enabled-done-button", "Done") + .on_click(|_, cx| cx.remove_window()), + ) + } // fn render_enabled_modal( // style: &theme::Copilot, @@ -280,7 +286,22 @@ impl CopilotCodeVerification { // .align_children_center() // .into_any() // } - + fn render_unauthorized_modal() -> impl Element { + v_stack() + .child(Label::new( + "Enable Copilot by connecting your existing license.", + )) + .child( + Label::new("You must have an active Copilot license to use it in Zed.") + .color(Color::Warning), + ) + .child( + Button::new("copilot-subscribe-button", "Subscibe on Github").on_click(|_, cx| { + cx.remove_window(); + cx.open_url(COPILOT_SIGN_UP_URL) + }), + ) + } // fn render_unauthorized_modal( // style: &theme::Copilot, // cx: &mut ViewContext, @@ -344,8 +365,18 @@ impl Render for CopilotCodeVerification { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let prompt = match &self.status { - Status::SigningIn { prompt } => prompt.as_ref(), - _ => None, + Status::SigningIn { + prompt: Some(prompt), + } => Self::render_prompting_modal(self.connect_clicked, &prompt, cx).into_any_element(), + Status::Unauthorized => { + self.connect_clicked = false; + Self::render_unauthorized_modal().into_any_element() + } + Status::Authorized => { + self.connect_clicked = false; + Self::render_enabled_modal().into_any_element() + } + _ => div().into_any_element(), }; div() .id("copilot code verification") @@ -357,9 +388,7 @@ impl Render for CopilotCodeVerification { .bg(cx.theme().colors().element_background) .child(ui::Label::new("Connect Copilot to Zed")) .child(IconElement::new(Icon::ZedXCopilot)) - .children( - prompt.map(|data| Self::render_prompting_modal(self.connect_clicked, data, cx)), - ) + .child(prompt) } } From 1b0ec82caa66bcabd55b2fde2fd9ba789fbd27ce Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 14:16:19 +0100 Subject: [PATCH 46/90] Remove old UI code, remove dbg! --- crates/copilot2/src/copilot2.rs | 1 - crates/copilot2/src/sign_in.rs | 241 +------------------------------- 2 files changed, 2 insertions(+), 240 deletions(-) diff --git a/crates/copilot2/src/copilot2.rs b/crates/copilot2/src/copilot2.rs index a829f68f41b266dc9a5896a2fffe2a926ac3c805..9c5483d634c201019873d9aa99fdaccd4b361cec 100644 --- a/crates/copilot2/src/copilot2.rs +++ b/crates/copilot2/src/copilot2.rs @@ -104,7 +104,6 @@ pub fn init( } }); cx.on_action(|_: &SignOut, cx| { - dbg!("Signing out"); if let Some(copilot) = Copilot::global(cx) { copilot .update(cx, |copilot, cx| copilot.sign_out(cx)) diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index dd107532fa1f55cf2baa9d5d1305c53f37814022..8da34c427f9eb98c4b2bee031761b4304b498f13 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -1,16 +1,3 @@ -// TODO add logging in -// use crate::{request::PromptUserDeviceFlow, Copilot, Status}; -// use gpui::{ -// elements::*, -// geometry::rect::RectF, -// platform::{WindowBounds, WindowKind, WindowOptions}, -// AnyElement, AnyViewHandle, AppContext, ClipboardItem, Element, Entity, View, ViewContext, -// WindowHandle, -// }; -// use theme::ui::modal; - -const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; - use crate::{request::PromptUserDeviceFlow, Copilot, Status}; use gpui::{ div, size, AppContext, Bounds, ClipboardItem, Div, Element, GlobalPixels, InteractiveElement, @@ -20,6 +7,8 @@ use gpui::{ use theme::ActiveTheme; use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label}; +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 verification_window: Option> = None; @@ -126,38 +115,6 @@ impl CopilotCodeVerification { .child(Label::new(data.user_code.clone())) .child(div()) .child(Label::new(if copied { "Copied!" } else { "Copy" })) - - // MouseEventHandler::new::(0, cx, |state, _cx| { - // Flex::row() - // .with_child( - // Label::new(data.user_code.clone(), device_code_style.text.clone()) - // .aligned() - // .contained() - // .with_style(device_code_style.left_container) - // .constrained() - // .with_width(device_code_style.left), - // ) - // .with_child( - // Label::new( - // if copied { "Copied!" } else { "Copy" }, - // device_code_style.cta.style_for(state).text.clone(), - // ) - // .aligned() - // .contained() - // .with_style(*device_code_style.right_container.style_for(state)) - // .constrained() - // .with_width(device_code_style.right), - // ) - // .contained() - // .with_style(device_code_style.cta.style_for(state).container) - // }) - // .on_click(gpui::platform::MouseButton::Left, { - // - // move |_, _, cx| { - // - // } - // }) - // .with_cursor_style(gpui::platform::CursorStyle::PointingHand) } fn render_prompting_modal( @@ -192,44 +149,6 @@ impl CopilotCodeVerification { }) }), ) - // Flex::column() - // .with_child(Self::render_device_code(data, &style, cx)) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Paste this code into GitHub after", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // Label::new( - // "clicking the button below.", - // style.auth.prompting.hint.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(style.auth.prompting.hint.container.clone()), - // ) - // .with_child(theme::ui::cta_button::( - // if connect_clicked { - // "Waiting for connection..." - // } else { - // "Connect to GitHub" - // }, - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // { - // let verification_uri = data.verification_uri.clone(); - // move |_, verification, cx| { - // cx.platform().open_url(&verification_uri); - // verification.connect_clicked = true; - // } - // }, - // )) - // .align_children_center() } fn render_enabled_modal() -> impl Element { v_stack() @@ -243,49 +162,6 @@ impl CopilotCodeVerification { ) } - // fn render_enabled_modal( - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> AnyElement { - // enum DoneButton {} - - // let enabled_style = &style.auth.authorized; - // Flex::column() - // .with_child( - // Label::new("Copilot Enabled!", enabled_style.subheading.text.clone()) - // .contained() - // .with_style(enabled_style.subheading.container) - // .aligned(), - // ) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "You can update your settings or", - // enabled_style.hint.text.clone(), - // ) - // .aligned(), - // Label::new( - // "sign out from the Copilot menu in", - // enabled_style.hint.text.clone(), - // ) - // .aligned(), - // Label::new("the status bar.", enabled_style.hint.text.clone()).aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(enabled_style.hint.container), - // ) - // .with_child(theme::ui::cta_button::( - // "Done", - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // |_, _, cx| cx.remove_window(), - // )) - // .align_children_center() - // .into_any() - // } fn render_unauthorized_modal() -> impl Element { v_stack() .child(Label::new( @@ -302,62 +178,6 @@ impl CopilotCodeVerification { }), ) } - // fn render_unauthorized_modal( - // style: &theme::Copilot, - // cx: &mut ViewContext, - // ) -> AnyElement { - // let unauthorized_style = &style.auth.not_authorized; - - // Flex::column() - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "Enable Copilot by connecting", - // unauthorized_style.subheading.text.clone(), - // ) - // .aligned(), - // Label::new( - // "your existing license.", - // unauthorized_style.subheading.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(unauthorized_style.subheading.container), - // ) - // .with_child( - // Flex::column() - // .with_children([ - // Label::new( - // "You must have an active copilot", - // unauthorized_style.warning.text.clone(), - // ) - // .aligned(), - // Label::new( - // "license to use it in Zed.", - // unauthorized_style.warning.text.clone(), - // ) - // .aligned(), - // ]) - // .align_children_center() - // .contained() - // .with_style(unauthorized_style.warning.container), - // ) - // .with_child(theme::ui::cta_button::( - // "Subscribe on GitHub", - // style.auth.content_width, - // &style.auth.cta_button, - // cx, - // |_, _, cx| { - // cx.remove_window(); - // cx.platform().open_url(COPILOT_SIGN_UP_URL) - // }, - // )) - // .align_children_center() - // .into_any() - // } } impl Render for CopilotCodeVerification { @@ -391,60 +211,3 @@ impl Render for CopilotCodeVerification { .child(prompt) } } - -// impl Entity for CopilotCodeVerification { -// type Event = (); -// } - -// impl View for CopilotCodeVerification { -// fn ui_name() -> &'static str { -// "CopilotCodeVerification" -// } - -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// cx.notify() -// } - -// fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// cx.notify() -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// enum ConnectModal {} - -// let style = theme::current(cx).clone(); - -// modal::( -// "Connect Copilot to Zed", -// &style.copilot.modal, -// cx, -// |cx| { -// Flex::column() -// .with_children([ -// theme::ui::icon(&style.copilot.auth.header).into_any(), -// match &self.status { -// Status::SigningIn { -// prompt: Some(prompt), -// } => Self::render_prompting_modal( -// self.connect_clicked, -// &prompt, -// &style.copilot, -// cx, -// ), -// Status::Unauthorized => { -// self.connect_clicked = false; -// Self::render_unauthorized_modal(&style.copilot, cx) -// } -// Status::Authorized => { -// self.connect_clicked = false; -// Self::render_enabled_modal(&style.copilot, cx) -// } -// _ => Empty::new().into_any(), -// }, -// ]) -// .align_children_center() -// }, -// ) -// .into_any() -// } -// } From 5f172a52a4f27bac0363eaa8913aaed4f2129335 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 14:23:05 +0100 Subject: [PATCH 47/90] Load assistant panel --- crates/workspace2/src/status_bar.rs | 23 ++++++------------ crates/zed2/src/zed2.rs | 37 +++++++++++++++-------------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 1bc84e04117d2fff8ba0d50188fb53e0cc0ca336..07c48293b5ca720dbc295003e2a3b578f999655c 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -62,22 +62,13 @@ impl Render for StatusBar { ) .child( // Right Dock - h_stack() - .gap_1() - .child( - // Terminal - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("status-assistant", Icon::Ai)), - ) - .child( - // Terminal - div() - .border() - .border_color(gpui::red()) - .child(IconButton::new("status-chat", Icon::MessageBubbles)), - ), + h_stack().gap_1().child( + // Terminal + div() + .border() + .border_color(gpui::red()) + .child(IconButton::new("status-chat", Icon::MessageBubbles)), + ), ) .child(self.render_right_tools(cx)), ) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 5f2099154cc8f298baedceeee3ab282fe8eca079..9cb3f5c0de44b1e788398529f10a1d69c35bec07 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -7,6 +7,7 @@ mod only_instance; mod open_listener; pub use assets::*; +use assistant::AssistantPanel; use breadcrumbs::Breadcrumbs; use collections::VecDeque; use editor::{Editor, MultiBuffer}; @@ -177,7 +178,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.spawn(|workspace_handle, mut cx| async move { let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); let channels_panel = collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); // let chat_panel = @@ -189,14 +190,14 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let ( project_panel, terminal_panel, - // assistant_panel, + assistant_panel, channels_panel, // chat_panel, // notification_panel, ) = futures::try_join!( project_panel, terminal_panel, - // assistant_panel, + assistant_panel, channels_panel, // chat_panel, // notification_panel, @@ -206,25 +207,25 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { let project_panel_position = project_panel.position(cx); workspace.add_panel(project_panel, cx); workspace.add_panel(terminal_panel, cx); - // workspace.add_panel(assistant_panel, cx); + workspace.add_panel(assistant_panel, cx); workspace.add_panel(channels_panel, cx); // workspace.add_panel(chat_panel, cx); // workspace.add_panel(notification_panel, cx); - // if !was_deserialized - // && workspace - // .project() - // .read(cx) - // .visible_worktrees(cx) - // .any(|tree| { - // tree.read(cx) - // .root_entry() - // .map_or(false, |entry| entry.is_dir()) - // }) - // { - // workspace.toggle_dock(project_panel_position, cx); - // } - // cx.focus_self(); + // if !was_deserialized + // && workspace + // .project() + // .read(cx) + // .visible_worktrees(cx) + // .any(|tree| { + // tree.read(cx) + // .root_entry() + // .map_or(false, |entry| entry.is_dir()) + // }) + // { + // workspace.toggle_dock(project_panel_position, cx); + // } + cx.focus_self(); }) }) .detach(); From 0ef97edd6edbab2cdf8b0028b6a82c06c26e81ea Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 16:57:16 +0200 Subject: [PATCH 48/90] Format the CI file with Zed's default prettier --- .github/workflows/ci.yml | 272 +++++++++++++------------- .github/workflows/release_nightly.yml | 160 +++++++-------- 2 files changed, 216 insertions(+), 216 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bfc0ab683e859f2675846499a518b8df220c7e0..c499b1933a26376458c8bd02376734fd7911a9ed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,144 +1,144 @@ name: CI on: - push: - branches: - - main - - "v[0-9]+.[0-9]+.x" - tags: - - "v*" - pull_request: - branches: - - "**" + push: + branches: + - main + - "v[0-9]+.[0-9]+.x" + tags: + - "v*" + pull_request: + branches: + - "**" env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 jobs: - rustfmt: - name: Check formatting - runs-on: - - self-hosted - - test - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Set up default .cargo/config.toml - run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml - - - name: Run rustfmt - uses: ./.github/actions/check_formatting - - tests: - name: Run tests - runs-on: - - self-hosted - - test - needs: rustfmt - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Run tests - uses: ./.github/actions/run_tests - - - name: Build collab - run: cargo build -p collab - - - name: Build other binaries - run: cargo build --workspace --bins --all-features - - bundle: - name: Bundle app - runs-on: - - self-hosted - - bundle - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} - needs: tests - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} - APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable - rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - rustup target add wasm32-wasi - - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: "18" - - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" - - - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 100 - - - name: Determine version and release channel - if: ${{ startsWith(github.ref, 'refs/tags/v') }} - run: | - set -eu - - version=$(script/get-crate-version zed) - channel=$(cat crates/zed/RELEASE_CHANNEL) - echo "Publishing version: ${version} on release channel ${channel}" - echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV - - expected_tag_name="" - case ${channel} in - stable) - expected_tag_name="v${version}";; - preview) - expected_tag_name="v${version}-pre";; - nightly) - expected_tag_name="v${version}-nightly";; - *) - echo "can't publish a release on channel ${channel}" - exit 1;; - esac - if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then - echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" - exit 1 - fi - - - name: Generate license file - run: script/generate-licenses - - - name: Create app bundle - run: script/bundle - - - name: Upload app bundle to workflow run if main branch or specific label - uses: actions/upload-artifact@v3 - if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} - with: - name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg - path: target/release/Zed.dmg - - - uses: softprops/action-gh-release@v1 - name: Upload app bundle to release - if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} - with: - draft: true - prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} - files: target/release/Zed.dmg - body: "" + rustfmt: + name: Check formatting + runs-on: + - self-hosted + - test + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Set up default .cargo/config.toml + run: cp ./.cargo/ci-config.toml ~/.cargo/config.toml + + - name: Run rustfmt + uses: ./.github/actions/check_formatting + + tests: + name: Run tests + runs-on: + - self-hosted + - test + needs: rustfmt + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Run tests + uses: ./.github/actions/run_tests + + - name: Build collab + run: cargo build -p collab + + - name: Build other binaries + run: cargo build --workspace --bins --all-features + + bundle: + name: Bundle app + runs-on: + - self-hosted + - bundle + if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + needs: tests env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} + APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + steps: + - name: Install Rust + run: | + rustup set profile minimal + rustup update stable + rustup target add aarch64-apple-darwin + rustup target add x86_64-apple-darwin + rustup target add wasm32-wasi + + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" + + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" + + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 100 + + - name: Determine version and release channel + if: ${{ startsWith(github.ref, 'refs/tags/v') }} + run: | + set -eu + + version=$(script/get-crate-version zed) + channel=$(cat crates/zed/RELEASE_CHANNEL) + echo "Publishing version: ${version} on release channel ${channel}" + echo "RELEASE_CHANNEL=${channel}" >> $GITHUB_ENV + + expected_tag_name="" + case ${channel} in + stable) + expected_tag_name="v${version}";; + preview) + expected_tag_name="v${version}-pre";; + nightly) + expected_tag_name="v${version}-nightly";; + *) + echo "can't publish a release on channel ${channel}" + exit 1;; + esac + if [[ $GITHUB_REF_NAME != $expected_tag_name ]]; then + echo "invalid release tag ${GITHUB_REF_NAME}. expected ${expected_tag_name}" + exit 1 + fi + + - name: Generate license file + run: script/generate-licenses + + - name: Create app bundle + run: script/bundle + + - name: Upload app bundle to workflow run if main branch or specific label + uses: actions/upload-artifact@v3 + if: ${{ github.ref == 'refs/heads/main' }} || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + with: + name: Zed_${{ github.event.pull_request.head.sha || github.sha }}.dmg + path: target/release/Zed.dmg + + - uses: softprops/action-gh-release@v1 + name: Upload app bundle to release + if: ${{ env.RELEASE_CHANNEL == 'preview' || env.RELEASE_CHANNEL == 'stable' }} + with: + draft: true + prerelease: ${{ env.RELEASE_CHANNEL == 'preview' }} + files: target/release/Zed.dmg + body: "" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 7b08c52c61395b09e9925ddb99a31470049b021b..38552646c343d5f44975e33146a4cd38dc3ab4d3 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -1,98 +1,98 @@ name: Release Nightly on: - schedule: - # Fire every night at 1:00am - - cron: "0 1 * * *" - push: - tags: - - "nightly" + schedule: + # Fire every night at 1:00am + - cron: "0 1 * * *" + push: + tags: + - "nightly" env: - CARGO_TERM_COLOR: always - CARGO_INCREMENTAL: 0 - RUST_BACKTRACE: 1 + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + RUST_BACKTRACE: 1 jobs: - rustfmt: - name: Check formatting - runs-on: - - self-hosted - - test - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" + rustfmt: + name: Check formatting + runs-on: + - self-hosted + - test + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Run rustfmt - uses: ./.github/actions/check_formatting + - name: Run rustfmt + uses: ./.github/actions/check_formatting - tests: - name: Run tests - runs-on: - - self-hosted - - test - needs: rustfmt - steps: - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" + tests: + name: Run tests + runs-on: + - self-hosted + - test + needs: rustfmt + steps: + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Run tests - uses: ./.github/actions/run_tests + - name: Run tests + uses: ./.github/actions/run_tests - bundle: - name: Bundle app - runs-on: - - self-hosted - - bundle - needs: tests - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} - APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} - APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} - DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} - steps: - - name: Install Rust - run: | - rustup set profile minimal - rustup update stable - rustup target add aarch64-apple-darwin - rustup target add x86_64-apple-darwin - rustup target add wasm32-wasi + bundle: + name: Bundle app + runs-on: + - self-hosted + - bundle + needs: tests + env: + MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} + MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_NOTARIZATION_USERNAME: ${{ secrets.APPLE_NOTARIZATION_USERNAME }} + APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} + DIGITALOCEAN_SPACES_ACCESS_KEY: ${{ secrets.DIGITALOCEAN_SPACES_ACCESS_KEY }} + DIGITALOCEAN_SPACES_SECRET_KEY: ${{ secrets.DIGITALOCEAN_SPACES_SECRET_KEY }} + steps: + - name: Install Rust + run: | + rustup set profile minimal + rustup update stable + rustup target add aarch64-apple-darwin + rustup target add x86_64-apple-darwin + rustup target add wasm32-wasi - - name: Install Node - uses: actions/setup-node@v3 - with: - node-version: "18" + - name: Install Node + uses: actions/setup-node@v3 + with: + node-version: "18" - - name: Checkout repo - uses: actions/checkout@v3 - with: - clean: false - submodules: "recursive" + - name: Checkout repo + uses: actions/checkout@v3 + with: + clean: false + submodules: "recursive" - - name: Limit target directory size - run: script/clear-target-dir-if-larger-than 100 + - name: Limit target directory size + run: script/clear-target-dir-if-larger-than 100 - - name: Set release channel to nightly - run: | - set -eu - version=$(git rev-parse --short HEAD) - echo "Publishing version: ${version} on release channel nightly" - echo "nightly" > crates/zed/RELEASE_CHANNEL + - name: Set release channel to nightly + run: | + set -eu + version=$(git rev-parse --short HEAD) + echo "Publishing version: ${version} on release channel nightly" + echo "nightly" > crates/zed/RELEASE_CHANNEL - - name: Generate license file - run: script/generate-licenses + - name: Generate license file + run: script/generate-licenses - - name: Create app bundle - run: script/bundle -2 + - name: Create app bundle + run: script/bundle -2 - - name: Upload Zed Nightly - run: script/upload-nightly + - name: Upload Zed Nightly + run: script/upload-nightly From a58f3934581e943fedce70579f674c5e528750c2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 16:58:49 +0200 Subject: [PATCH 49/90] Do not bundle Zed on `main` branch commits --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c499b1933a26376458c8bd02376734fd7911a9ed..8ac2912424bb8bf10014ac1b8961405cd2ba47c5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: runs-on: - self-hosted - bundle - if: ${{ github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} + if: ${{ startsWith(github.ref, 'refs/tags/v') || contains(github.event.pull_request.labels.*.name, 'run-build-dmg') }} needs: tests env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} From d09dfe01f5ad14f864063efd660f551f4377fff5 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:15:53 +0100 Subject: [PATCH 50/90] Wire up global actions Added an ephemeral root node so that even if there's no window/focused handle we still have something to dispatch to. Co-authored-by: Antonio --- crates/editor2/src/element.rs | 63 ++++++----- crates/gpui2/src/app.rs | 6 +- crates/gpui2/src/elements/div.rs | 6 +- crates/gpui2/src/key_dispatch.rs | 25 +++-- crates/gpui2/src/window.rs | 186 +++++++++++++++++-------------- 5 files changed, 158 insertions(+), 128 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d7b9d0bb40498cd8fb6c51f4cd33d5e6489f4ad1..ab11f5ffb5427d39905675756be5d9996e984fc1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2803,35 +2803,46 @@ impl Element for EditorElement { let focus_handle = editor.focus_handle(cx); let dispatch_context = self.editor.read(cx).dispatch_context(cx); - cx.with_key_dispatch(dispatch_context, Some(focus_handle.clone()), |_, cx| { - self.register_actions(cx); - self.register_key_listeners(cx); - - // We call with_z_index to establish a new stacking context. - cx.with_z_index(0, |cx| { - cx.with_content_mask(Some(ContentMask { bounds }), |cx| { - // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor - // take precedence. - cx.with_z_index(0, |cx| { - self.paint_mouse_listeners(bounds, gutter_bounds, text_bounds, &layout, cx); - }); - let input_handler = ElementInputHandler::new(bounds, self.editor.clone(), cx); - cx.handle_input(&focus_handle, input_handler); + cx.with_key_dispatch( + Some(dispatch_context), + Some(focus_handle.clone()), + |_, cx| { + self.register_actions(cx); + self.register_key_listeners(cx); + + // We call with_z_index to establish a new stacking context. + cx.with_z_index(0, |cx| { + cx.with_content_mask(Some(ContentMask { bounds }), |cx| { + // Paint mouse listeners at z-index 0 so any elements we paint on top of the editor + // take precedence. + cx.with_z_index(0, |cx| { + self.paint_mouse_listeners( + bounds, + gutter_bounds, + text_bounds, + &layout, + cx, + ); + }); + let input_handler = + ElementInputHandler::new(bounds, self.editor.clone(), cx); + cx.handle_input(&focus_handle, input_handler); - self.paint_background(gutter_bounds, text_bounds, &layout, cx); - if layout.gutter_size.width > Pixels::ZERO { - self.paint_gutter(gutter_bounds, &mut layout, cx); - } - self.paint_text(text_bounds, &mut layout, cx); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); + if layout.gutter_size.width > Pixels::ZERO { + self.paint_gutter(gutter_bounds, &mut layout, cx); + } + self.paint_text(text_bounds, &mut layout, cx); - if !layout.blocks.is_empty() { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); - }) - } + if !layout.blocks.is_empty() { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }) + } + }); }); - }); - }) + }, + ) } } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index fec6f150f6c341f916e0173379aba63bebcc1ffd..4803eb8b97c8cf86bfb56843f739670e1f81cb52 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -201,7 +201,7 @@ pub struct AppContext { pub(crate) windows: SlotMap>, pub(crate) keymap: Arc>, pub(crate) global_action_listeners: - HashMap>>, + HashMap>>, pending_effects: VecDeque, pub(crate) pending_notifications: HashSet, pub(crate) pending_global_notifications: HashSet, @@ -962,9 +962,9 @@ impl AppContext { self.global_action_listeners .entry(TypeId::of::()) .or_default() - .push(Box::new(move |action, phase, cx| { + .push(Rc::new(move |action, phase, cx| { if phase == DispatchPhase::Bubble { - let action = action.as_any().downcast_ref().unwrap(); + let action = action.downcast_ref().unwrap(); listener(action, cx) } })); diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index ce457fc6931246ee1e2e4d19a4a1639b37998395..c95a7f890f986b19ba7fdbac6b35c36ed8db36df 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -55,7 +55,7 @@ pub trait InteractiveElement: Sized + Element { E: Debug, { if let Some(key_context) = key_context.try_into().log_err() { - self.interactivity().key_context = key_context; + self.interactivity().key_context = Some(key_context); } self } @@ -722,7 +722,7 @@ impl DivState { pub struct Interactivity { pub element_id: Option, - pub key_context: KeyContext, + pub key_context: Option, pub focusable: bool, pub tracked_focus_handle: Option, pub scroll_handle: Option, @@ -1238,7 +1238,7 @@ impl Default for Interactivity { fn default() -> Self { Self { element_id: None, - key_context: KeyContext::default(), + key_context: None, focusable: false, tracked_focus_handle: None, scroll_handle: None, diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 4838b1a612ce65ba33c03ac25da878a752f716d3..a79a358a1c38453fe7a8501e1ae0cae5ccf832c3 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -61,7 +61,7 @@ impl DispatchTree { self.keystroke_matchers.clear(); } - pub fn push_node(&mut self, context: KeyContext) { + pub fn push_node(&mut self, context: Option) { let parent = self.node_stack.last().copied(); let node_id = DispatchNodeId(self.nodes.len()); self.nodes.push(DispatchNode { @@ -69,7 +69,7 @@ impl DispatchTree { ..Default::default() }); self.node_stack.push(node_id); - if !context.is_empty() { + if let Some(context) = context { self.active_node().context = context.clone(); self.context_stack.push(context); } @@ -148,16 +148,14 @@ impl DispatchTree { false } - pub fn available_actions(&self, target: FocusId) -> Vec> { + pub fn available_actions(&self, target: DispatchNodeId) -> Vec> { let mut actions = Vec::new(); - if let Some(node) = self.focusable_node_ids.get(&target) { - for node_id in self.dispatch_path(*node) { - let node = &self.nodes[node_id.0]; - for DispatchActionListener { action_type, .. } in &node.action_listeners { - // Intentionally silence these errors without logging. - // If an action cannot be built by default, it's not available. - actions.extend(self.action_registry.build_action_type(action_type).ok()); - } + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + for DispatchActionListener { action_type, .. } in &node.action_listeners { + // Intentionally silence these errors without logging. + // If an action cannot be built by default, it's not available. + actions.extend(self.action_registry.build_action_type(action_type).ok()); } } actions @@ -236,6 +234,11 @@ impl DispatchTree { self.focusable_node_ids.get(&target).copied() } + pub fn root_node_id(&self) -> DispatchNodeId { + debug_assert!(!self.nodes.is_empty()); + DispatchNodeId(0) + } + fn active_node_id(&self) -> DispatchNodeId { *self.node_stack.last().unwrap() } diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 271b09b8b950d777400f403fa59d5b81ad4e5ed6..6323eb962f101daa66fcfa8ae1482408507b7670 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -453,19 +453,21 @@ impl<'a> WindowContext<'a> { } pub fn dispatch_action(&mut self, action: Box) { - if let Some(focus_handle) = self.focused() { - self.defer(move |cx| { - if let Some(node_id) = cx - .window - .current_frame - .dispatch_tree - .focusable_node_id(focus_handle.id) - { - cx.propagate_event = true; - cx.dispatch_action_on_node(node_id, action); - } - }) - } + let focus_handle = self.focused(); + + self.defer(move |cx| { + let node_id = focus_handle + .and_then(|handle| { + cx.window + .current_frame + .dispatch_tree + .focusable_node_id(handle.id) + }) + .unwrap_or_else(|| cx.window.current_frame.dispatch_tree.root_node_id()); + + cx.propagate_event = true; + cx.dispatch_action_on_node(node_id, action); + }) } /// Schedules the given function to be run at the end of the current effect cycle, allowing entities @@ -1154,8 +1156,19 @@ impl<'a> WindowContext<'a> { self.start_frame(); self.with_z_index(0, |cx| { - let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(Point::zero(), available_space, cx); + cx.with_key_dispatch(Some(KeyContext::default()), None, |_, cx| { + for (action_type, action_listeners) in &cx.app.global_action_listeners { + for action_listener in action_listeners.iter().cloned() { + cx.window.current_frame.dispatch_tree.on_action( + *action_type, + Rc::new(move |action, phase, cx| action_listener(action, phase, cx)), + ) + } + } + + let available_space = cx.window.viewport_size.map(Into::into); + root_view.draw(Point::zero(), available_space, cx); + }) }); if let Some(active_drag) = self.app.active_drag.take() { @@ -1338,73 +1351,77 @@ impl<'a> WindowContext<'a> { } fn dispatch_key_event(&mut self, event: &dyn Any) { - if let Some(node_id) = self.window.focus.and_then(|focus_id| { - self.window - .current_frame - .dispatch_tree - .focusable_node_id(focus_id) - }) { - let dispatch_path = self - .window - .current_frame - .dispatch_tree - .dispatch_path(node_id); + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id()); - let mut actions: Vec> = Vec::new(); + let dispatch_path = self + .window + .current_frame + .dispatch_tree + .dispatch_path(node_id); - // Capture phase - let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); - self.propagate_event = true; + let mut actions: Vec> = Vec::new(); - for node_id in &dispatch_path { - let node = self.window.current_frame.dispatch_tree.node(*node_id); + // Capture phase + let mut context_stack: SmallVec<[KeyContext; 16]> = SmallVec::new(); + self.propagate_event = true; - if !node.context.is_empty() { - context_stack.push(node.context.clone()); - } + for node_id in &dispatch_path { + let node = self.window.current_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Capture, self); - if !self.propagate_event { - return; - } + if !node.context.is_empty() { + context_stack.push(node.context.clone()); + } + + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Capture, self); + if !self.propagate_event { + return; } } + } - // Bubble phase - for node_id in dispatch_path.iter().rev() { - // Handle low level key events - let node = self.window.current_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { - key_listener(event, DispatchPhase::Bubble, self); - if !self.propagate_event { - return; - } + // Bubble phase + for node_id in dispatch_path.iter().rev() { + // Handle low level key events + let node = self.window.current_frame.dispatch_tree.node(*node_id); + for key_listener in node.key_listeners.clone() { + key_listener(event, DispatchPhase::Bubble, self); + if !self.propagate_event { + return; } + } - // Match keystrokes - let node = self.window.current_frame.dispatch_tree.node(*node_id); - if !node.context.is_empty() { - if let Some(key_down_event) = event.downcast_ref::() { - if let Some(found) = self - .window - .current_frame - .dispatch_tree - .dispatch_key(&key_down_event.keystroke, &context_stack) - { - actions.push(found.boxed_clone()) - } + // Match keystrokes + let node = self.window.current_frame.dispatch_tree.node(*node_id); + if !node.context.is_empty() { + if let Some(key_down_event) = event.downcast_ref::() { + if let Some(found) = self + .window + .current_frame + .dispatch_tree + .dispatch_key(&key_down_event.keystroke, &context_stack) + { + actions.push(found.boxed_clone()) } - - context_stack.pop(); } + + context_stack.pop(); } + } - for action in actions { - self.dispatch_action_on_node(node_id, action); - if !self.propagate_event { - return; - } + for action in actions { + self.dispatch_action_on_node(node_id, action); + if !self.propagate_event { + return; } } } @@ -1490,22 +1507,21 @@ impl<'a> WindowContext<'a> { } pub fn available_actions(&self) -> Vec> { - if let Some(focus_id) = self.window.focus { - let mut actions = self - .window - .current_frame - .dispatch_tree - .available_actions(focus_id); - actions.extend( - self.app - .global_action_listeners - .keys() - .filter_map(|type_id| self.app.actions.build_action_type(type_id).ok()), - ); - actions - } else { - Vec::new() - } + let node_id = self + .window + .focus + .and_then(|focus_id| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focus_id) + }) + .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id()); + + self.window + .current_frame + .dispatch_tree + .available_actions(node_id) } pub fn bindings_for_action(&self, action: &dyn Action) -> Vec { @@ -1561,7 +1577,7 @@ impl<'a> WindowContext<'a> { //========== ELEMENT RELATED FUNCTIONS =========== pub fn with_key_dispatch( &mut self, - context: KeyContext, + context: Option, focus_handle: Option, f: impl FnOnce(Option, &mut Self) -> R, ) -> R { From a1c8f01ff3592130dc9dce77388617d00b9b02e1 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 16:34:07 +0100 Subject: [PATCH 51/90] WIP --- crates/assistant2/src/assistant_panel.rs | 38 ++++++++++++++---------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index c9a9d0d2519536f921d667e1f4e5434cd04b1973..2a589afc4f519fa51e2567d55851443f1a538703 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -1140,6 +1140,7 @@ impl Render for AssistantPanel { } v_stack() + .size_full() .on_action(cx.listener(|this, _: &workspace::NewFile, cx| { this.new_conversation(cx); })) @@ -1156,22 +1157,26 @@ impl Render for AssistantPanel { } else { Some(self.toolbar.clone()) }) - .child(if let Some(editor) = self.active_editor() { - editor.clone().into_any_element() - } else { - uniform_list( - cx.view().clone(), - "saved_conversations", - self.saved_conversations.len(), - |this, range, cx| { - range - .map(|ix| this.render_saved_conversation(ix, cx)) - .collect() - }, - ) - .track_scroll(self.saved_conversations_scroll_handle.clone()) - .into_any_element() - }) + .child( + div() + .flex_1() + .child(if let Some(editor) = self.active_editor() { + editor.clone().into_any_element() + } else { + uniform_list( + cx.view().clone(), + "saved_conversations", + self.saved_conversations.len(), + |this, range, cx| { + range + .map(|ix| this.render_saved_conversation(ix, cx)) + .collect() + }, + ) + .track_scroll(self.saved_conversations_scroll_handle.clone()) + .into_any_element() + }), + ) .border() .border_color(gpui::red()) } @@ -2469,6 +2474,7 @@ impl Render for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() + .size_full() .relative() .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) .capture_action(cx.listener(ConversationEditor::save)) From f833cd7c160239b9f72e1ca394cdd6209e35ede2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 10:41:49 -0500 Subject: [PATCH 52/90] Use specified color for non-highlighted text in `HighlightedLabel` (#3509) This PR fixes an issue where the specified color for a `HighlightedLabel` was not respected as the default color for non-highlighted text. Release Notes: - N/A --- crates/ui2/src/components/label.rs | 5 ++++- crates/ui2/src/components/stories/label.rs | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 7aeda3e850fbe91463f6f3f6a0298cb3fc7fa5a7..d455a041ee6e8f861a3cdacf1c155c2f7fc57871 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -129,6 +129,9 @@ impl RenderOnce for HighlightedLabel { )); } + let mut text_style = cx.text_style().clone(); + text_style.color = self.color.color(cx); + div() .flex() .when(self.strikethrough, |this| { @@ -146,7 +149,7 @@ impl RenderOnce for HighlightedLabel { LabelSize::Default => this.text_ui(), LabelSize::Small => this.text_ui_sm(), }) - .child(StyledText::new(self.label).with_highlights(&cx.text_style(), highlights)) + .child(StyledText::new(self.label).with_highlights(&text_style, highlights)) } } diff --git a/crates/ui2/src/components/stories/label.rs b/crates/ui2/src/components/stories/label.rs index 2417bee6e1cd77646ceddd2c4febe4c6e45b7e19..e026d388fde62545f47aab22985fa7efba4f3c7a 100644 --- a/crates/ui2/src/components/stories/label.rs +++ b/crates/ui2/src/components/stories/label.rs @@ -23,5 +23,9 @@ impl Render for LabelStory { "Héllo, world!", vec![0, 1, 3, 8, 9, 13], )) + .child(Story::label("Highlighted with `color`")) + .child( + HighlightedLabel::new("Hello, world!", vec![0, 1, 2, 7, 8, 12]).color(Color::Error), + ) } } From 6549a9a091512f6db53e08efbd3a94d53d524f00 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 6 Dec 2023 16:52:52 +0100 Subject: [PATCH 53/90] Let WindowContext::dispatch_action handle global actions Co-authored-by: Antonio --- crates/gpui2/src/app.rs | 82 ++++++++++++++------------------ crates/gpui2/src/key_dispatch.rs | 20 ++++---- crates/gpui2/src/window.rs | 16 +++++++ 3 files changed, 60 insertions(+), 58 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 0abdf4b17410419529c705bfd808271f5100a7c0..d23d6e3d9d15003ab3236a6d491143f25d1f48de 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1040,20 +1040,10 @@ impl AppContext { pub fn is_action_available(&mut self, action: &dyn Action) -> bool { if let Some(window) = self.active_window() { - let window_action_available = window - .update(self, |_, cx| { - if let Some(focus_id) = cx.window.focus { - cx.window - .current_frame - .dispatch_tree - .is_action_available(action, focus_id) - } else { - false - } - }) - .unwrap_or(false); - if window_action_available { - return true; + if let Ok(window_action_available) = + window.update(self, |_, cx| cx.is_action_available(action)) + { + return window_action_available; } } @@ -1075,44 +1065,19 @@ impl AppContext { } pub fn dispatch_action(&mut self, action: &dyn Action) { - self.propagate_event = true; - - if let Some(mut global_listeners) = self - .global_action_listeners - .remove(&action.as_any().type_id()) - { - for listener in &global_listeners { - listener(action, DispatchPhase::Capture, self); - if !self.propagate_event { - break; - } - } - - global_listeners.extend( - self.global_action_listeners - .remove(&action.as_any().type_id()) - .unwrap_or_default(), - ); - - self.global_action_listeners - .insert(action.as_any().type_id(), global_listeners); - } - - if self.propagate_event { - if let Some(active_window) = self.active_window() { - active_window - .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) - .log_err(); - } - } + if let Some(active_window) = self.active_window() { + active_window + .update(self, |_, cx| cx.dispatch_action(action.boxed_clone())) + .log_err(); + } else { + self.propagate_event = true; - if self.propagate_event { if let Some(mut global_listeners) = self .global_action_listeners .remove(&action.as_any().type_id()) { - for listener in global_listeners.iter().rev() { - listener(action, DispatchPhase::Bubble, self); + for listener in &global_listeners { + listener(action.as_any(), DispatchPhase::Capture, self); if !self.propagate_event { break; } @@ -1127,6 +1092,29 @@ impl AppContext { self.global_action_listeners .insert(action.as_any().type_id(), global_listeners); } + + if self.propagate_event { + if let Some(mut global_listeners) = self + .global_action_listeners + .remove(&action.as_any().type_id()) + { + for listener in global_listeners.iter().rev() { + listener(action.as_any(), DispatchPhase::Bubble, self); + if !self.propagate_event { + break; + } + } + + global_listeners.extend( + self.global_action_listeners + .remove(&action.as_any().type_id()) + .unwrap_or_default(), + ); + + self.global_action_listeners + .insert(action.as_any().type_id(), global_listeners); + } + } } } } diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 07561644f7a36164aa328349aaf7c08ad2c74621..80e662ad3ef6477b4cd0d925b2852957a4c04d00 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -161,17 +161,15 @@ impl DispatchTree { actions } - pub fn is_action_available(&self, action: &dyn Action, target: FocusId) -> bool { - if let Some(node) = self.focusable_node_ids.get(&target) { - for node_id in self.dispatch_path(*node) { - let node = &self.nodes[node_id.0]; - if node - .action_listeners - .iter() - .any(|listener| listener.action_type == action.as_any().type_id()) - { - return true; - } + pub fn is_action_available(&self, action: &dyn Action, target: DispatchNodeId) -> bool { + for node_id in self.dispatch_path(target) { + let node = &self.nodes[node_id.0]; + if node + .action_listeners + .iter() + .any(|listener| listener.action_type == action.as_any().type_id()) + { + return true; } } false diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 1bd8b4653797669519349a0edb6fb783c361c392..8995d04b64de4abac4653e228691f3784eb89a6e 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -804,6 +804,22 @@ impl<'a> WindowContext<'a> { ); } + pub fn is_action_available(&self, action: &dyn Action) -> bool { + let target = self + .focused() + .and_then(|focused_handle| { + self.window + .current_frame + .dispatch_tree + .focusable_node_id(focused_handle.id) + }) + .unwrap_or_else(|| self.window.current_frame.dispatch_tree.root_node_id()); + self.window + .current_frame + .dispatch_tree + .is_action_available(action, target) + } + /// The position of the mouse relative to the window. pub fn mouse_position(&self) -> Point { self.window.mouse_position From 8f1c74b8bc0cee150b33b76309b9f21735bfda4f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 11:17:12 -0500 Subject: [PATCH 54/90] Factor out `LabelLike` to share common label styles (#3510) This PR factors out a new `LabelLike` component to share common styles between the `Label` and `HighlightedLabel` components. Release Notes: - N/A --- crates/copilot2/src/sign_in.rs | 2 +- crates/diagnostics2/src/diagnostics.rs | 2 +- crates/editor2/src/items.rs | 2 +- crates/go_to_line2/src/go_to_line.rs | 2 +- crates/ui2/src/components/label.rs | 188 +----------------- .../src/components/label/highlighted_label.rs | 86 ++++++++ crates/ui2/src/components/label/label.rs | 48 +++++ crates/ui2/src/components/label/label_like.rs | 102 ++++++++++ crates/ui2/src/prelude.rs | 2 +- 9 files changed, 247 insertions(+), 187 deletions(-) create mode 100644 crates/ui2/src/components/label/highlighted_label.rs create mode 100644 crates/ui2/src/components/label/label.rs create mode 100644 crates/ui2/src/components/label/label_like.rs diff --git a/crates/copilot2/src/sign_in.rs b/crates/copilot2/src/sign_in.rs index 8da34c427f9eb98c4b2bee031761b4304b498f13..4fa93ffcf89a496ae1b150334c9b7a310b779cec 100644 --- a/crates/copilot2/src/sign_in.rs +++ b/crates/copilot2/src/sign_in.rs @@ -5,7 +5,7 @@ use gpui::{ WindowBounds, WindowHandle, WindowKind, WindowOptions, }; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Button, Clickable, Color, Icon, IconElement, Label}; +use ui::{prelude::*, Button, Icon, IconElement, Label}; const COPILOT_SIGN_UP_URL: &'static str = "https://github.com/features/copilot"; diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs index 44acc285e8231a48e20c791f38f09f6619e99d08..f725fb7c4f58906f4263a5bcc86e6dd1ec24af6f 100644 --- a/crates/diagnostics2/src/diagnostics.rs +++ b/crates/diagnostics2/src/diagnostics.rs @@ -36,7 +36,7 @@ use std::{ }; use theme::ActiveTheme; pub use toolbar_controls::ToolbarControls; -use ui::{h_stack, Color, HighlightedLabel, Icon, IconElement, Label}; +use ui::{h_stack, prelude::*, HighlightedLabel, Icon, IconElement, Label}; use util::TryFutureExt; use workspace::{ item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 70d4d6bf255fad8b056a19ba5d5b41dc97a66178..12feb31696922558d61eb00d0a995039a21b2cea 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -32,7 +32,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{h_stack, Color, Label}; +use ui::{h_stack, prelude::*, Label}; use util::{paths::PathExt, paths::FILE_ROW_COLUMN_DELIMITER, ResultExt, TryFutureExt}; use workspace::{ item::{BreadcrumbText, FollowEvent, FollowableItemHandle}, diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 5ad95c1f6ea6cf0fb49e185a044d8be7eaf383fe..aff9942c265da765431460d9f381abf131e3573a 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -6,7 +6,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Color, Label, StyledExt}; +use ui::{h_stack, prelude::*, v_stack, Label}; use util::paths::FILE_ROW_COLUMN_DELIMITER; actions!(Toggle); diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index d455a041ee6e8f861a3cdacf1c155c2f7fc57871..bda97be6490d20309941e9de980f4a0fb2350bb1 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -1,183 +1,7 @@ -use std::ops::Range; +mod highlighted_label; +mod label; +mod label_like; -use crate::prelude::*; -use crate::styled_ext::StyledExt; -use gpui::{relative, Div, HighlightStyle, IntoElement, StyledText, WindowContext}; - -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] -pub enum LabelSize { - #[default] - Default, - Small, -} - -#[derive(Default, PartialEq, Copy, Clone)] -pub enum LineHeightStyle { - #[default] - TextLabel, - /// Sets the line height to 1 - UILabel, -} - -#[derive(IntoElement, Clone)] -pub struct Label { - label: SharedString, - size: LabelSize, - line_height_style: LineHeightStyle, - color: Color, - strikethrough: bool, -} - -impl RenderOnce for Label { - type Rendered = Div; - - fn render(self, cx: &mut WindowContext) -> Self::Rendered { - div() - .when(self.strikethrough, |this| { - this.relative().child( - div() - .absolute() - .top_1_2() - .w_full() - .h_px() - .bg(Color::Hidden.color(cx)), - ) - }) - .map(|this| match self.size { - LabelSize::Default => this.text_ui(), - LabelSize::Small => this.text_ui_sm(), - }) - .when(self.line_height_style == LineHeightStyle::UILabel, |this| { - this.line_height(relative(1.)) - }) - .text_color(self.color.color(cx)) - .child(self.label.clone()) - } -} - -impl Label { - pub fn new(label: impl Into) -> Self { - Self { - label: label.into(), - size: LabelSize::Default, - line_height_style: LineHeightStyle::default(), - color: Color::Default, - strikethrough: false, - } - } - - pub fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { - self.line_height_style = line_height_style; - self - } - - pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { - self.strikethrough = strikethrough; - self - } -} - -#[derive(IntoElement)] -pub struct HighlightedLabel { - label: SharedString, - size: LabelSize, - color: Color, - highlight_indices: Vec, - strikethrough: bool, -} - -impl RenderOnce for HighlightedLabel { - type Rendered = Div; - - fn render(self, cx: &mut WindowContext) -> Self::Rendered { - let highlight_color = cx.theme().colors().text_accent; - - let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); - let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); - - while let Some(start_ix) = highlight_indices.next() { - let mut end_ix = start_ix; - - loop { - end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8(); - if let Some(&next_ix) = highlight_indices.peek() { - if next_ix == end_ix { - end_ix = next_ix; - highlight_indices.next(); - continue; - } - } - break; - } - - highlights.push(( - start_ix..end_ix, - HighlightStyle { - color: Some(highlight_color), - ..Default::default() - }, - )); - } - - let mut text_style = cx.text_style().clone(); - text_style.color = self.color.color(cx); - - div() - .flex() - .when(self.strikethrough, |this| { - this.relative().child( - div() - .absolute() - .top_px() - .my_auto() - .w_full() - .h_px() - .bg(Color::Hidden.color(cx)), - ) - }) - .map(|this| match self.size { - LabelSize::Default => this.text_ui(), - LabelSize::Small => this.text_ui_sm(), - }) - .child(StyledText::new(self.label).with_highlights(&text_style, highlights)) - } -} - -impl HighlightedLabel { - /// shows a label with the given characters highlighted. - /// characters are identified by utf8 byte position. - pub fn new(label: impl Into, highlight_indices: Vec) -> Self { - Self { - label: label.into(), - size: LabelSize::Default, - color: Color::Default, - highlight_indices, - strikethrough: false, - } - } - - pub fn size(mut self, size: LabelSize) -> Self { - self.size = size; - self - } - - pub fn color(mut self, color: Color) -> Self { - self.color = color; - self - } - - pub fn set_strikethrough(mut self, strikethrough: bool) -> Self { - self.strikethrough = strikethrough; - self - } -} +pub use highlighted_label::*; +pub use label::*; +pub use label_like::*; diff --git a/crates/ui2/src/components/label/highlighted_label.rs b/crates/ui2/src/components/label/highlighted_label.rs new file mode 100644 index 0000000000000000000000000000000000000000..a7fbb0d8167cd0f78fd40f01454621a3e26ca33d --- /dev/null +++ b/crates/ui2/src/components/label/highlighted_label.rs @@ -0,0 +1,86 @@ +use std::ops::Range; + +use gpui::{HighlightStyle, StyledText}; + +use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; + +#[derive(IntoElement)] +pub struct HighlightedLabel { + base: LabelLike, + label: SharedString, + highlight_indices: Vec, +} + +impl HighlightedLabel { + /// Constructs a label with the given characters highlighted. + /// Characters are identified by UTF-8 byte position. + pub fn new(label: impl Into, highlight_indices: Vec) -> Self { + Self { + base: LabelLike::new(), + label: label.into(), + highlight_indices, + } + } +} + +impl LabelCommon for HighlightedLabel { + fn size(mut self, size: LabelSize) -> Self { + self.base = self.base.size(size); + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.base = self.base.line_height_style(line_height_style); + self + } + + fn color(mut self, color: Color) -> Self { + self.base = self.base.color(color); + self + } + + fn strikethrough(mut self, strikethrough: bool) -> Self { + self.base = self.base.strikethrough(strikethrough); + self + } +} + +impl RenderOnce for HighlightedLabel { + type Rendered = LabelLike; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + let highlight_color = cx.theme().colors().text_accent; + + let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); + let mut highlights: Vec<(Range, HighlightStyle)> = Vec::new(); + + while let Some(start_ix) = highlight_indices.next() { + let mut end_ix = start_ix; + + loop { + end_ix = end_ix + self.label[end_ix..].chars().next().unwrap().len_utf8(); + if let Some(&next_ix) = highlight_indices.peek() { + if next_ix == end_ix { + end_ix = next_ix; + highlight_indices.next(); + continue; + } + } + break; + } + + highlights.push(( + start_ix..end_ix, + HighlightStyle { + color: Some(highlight_color), + ..Default::default() + }, + )); + } + + let mut text_style = cx.text_style().clone(); + text_style.color = self.base.color.color(cx); + + LabelLike::new().child(StyledText::new(self.label).with_highlights(&text_style, highlights)) + } +} diff --git a/crates/ui2/src/components/label/label.rs b/crates/ui2/src/components/label/label.rs new file mode 100644 index 0000000000000000000000000000000000000000..8272340888837ca3f2ec2a0294a3168c14009cd0 --- /dev/null +++ b/crates/ui2/src/components/label/label.rs @@ -0,0 +1,48 @@ +use gpui::WindowContext; + +use crate::{prelude::*, LabelCommon, LabelLike, LabelSize, LineHeightStyle}; + +#[derive(IntoElement)] +pub struct Label { + base: LabelLike, + label: SharedString, +} + +impl Label { + pub fn new(label: impl Into) -> Self { + Self { + base: LabelLike::new(), + label: label.into(), + } + } +} + +impl LabelCommon for Label { + fn size(mut self, size: LabelSize) -> Self { + self.base = self.base.size(size); + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.base = self.base.line_height_style(line_height_style); + self + } + + fn color(mut self, color: Color) -> Self { + self.base = self.base.color(color); + self + } + + fn strikethrough(mut self, strikethrough: bool) -> Self { + self.base = self.base.strikethrough(strikethrough); + self + } +} + +impl RenderOnce for Label { + type Rendered = LabelLike; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + self.base.child(self.label) + } +} diff --git a/crates/ui2/src/components/label/label_like.rs b/crates/ui2/src/components/label/label_like.rs new file mode 100644 index 0000000000000000000000000000000000000000..72a48adea451f49cd10dd2e2bd224f16d4ae51a9 --- /dev/null +++ b/crates/ui2/src/components/label/label_like.rs @@ -0,0 +1,102 @@ +use gpui::{relative, AnyElement, Div, Styled}; +use smallvec::SmallVec; + +use crate::prelude::*; + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum LabelSize { + #[default] + Default, + Small, +} + +#[derive(Default, PartialEq, Copy, Clone)] +pub enum LineHeightStyle { + #[default] + TextLabel, + /// Sets the line height to 1 + UILabel, +} + +pub trait LabelCommon { + fn size(self, size: LabelSize) -> Self; + fn line_height_style(self, line_height_style: LineHeightStyle) -> Self; + fn color(self, color: Color) -> Self; + fn strikethrough(self, strikethrough: bool) -> Self; +} + +#[derive(IntoElement)] +pub struct LabelLike { + size: LabelSize, + line_height_style: LineHeightStyle, + pub(crate) color: Color, + strikethrough: bool, + children: SmallVec<[AnyElement; 2]>, +} + +impl LabelLike { + pub fn new() -> Self { + Self { + size: LabelSize::Default, + line_height_style: LineHeightStyle::default(), + color: Color::Default, + strikethrough: false, + children: SmallVec::new(), + } + } +} + +impl LabelCommon for LabelLike { + fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + + fn line_height_style(mut self, line_height_style: LineHeightStyle) -> Self { + self.line_height_style = line_height_style; + self + } + + fn color(mut self, color: Color) -> Self { + self.color = color; + self + } + + fn strikethrough(mut self, strikethrough: bool) -> Self { + self.strikethrough = strikethrough; + self + } +} + +impl ParentElement for LabelLike { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl RenderOnce for LabelLike { + type Rendered = Div; + + fn render(self, cx: &mut WindowContext) -> Self::Rendered { + div() + .when(self.strikethrough, |this| { + this.relative().child( + div() + .absolute() + .top_1_2() + .w_full() + .h_px() + .bg(Color::Hidden.color(cx)), + ) + }) + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) + .when(self.line_height_style == LineHeightStyle::UILabel, |this| { + this.line_height(relative(1.)) + }) + .text_color(self.color.color(cx)) + .children(self.children) + } +} diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 38065b62754b5facb5ed9440ad2b74535f40d445..a71efa4dc3ebc8e4065260575beac78972f3f89c 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -9,5 +9,5 @@ pub use crate::disableable::*; pub use crate::fixed::*; pub use crate::selectable::*; pub use crate::{h_stack, v_stack}; -pub use crate::{ButtonCommon, Color, StyledExt}; +pub use crate::{ButtonCommon, Color, LabelCommon, StyledExt}; pub use theme::ActiveTheme; From c8ddc95caa4860948fd6f9a7dc7801cc828a6b64 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:26:54 +0100 Subject: [PATCH 55/90] Take a `Keymap` when setting app menus For a brief period on this branch, we were taking a `DispatchTree`. Doing so resulted in more accurate key bindings but it meant that we would have had to recompute the app menus every time the key context changed. We decided to err on the side of keeping things simple and work in the same way they worked back in zed1. Co-Authored-By: Marshall --- crates/gpui2/src/app.rs | 11 +---------- crates/gpui2/src/platform.rs | 6 +++--- crates/gpui2/src/platform/mac/platform.rs | 23 ++++++++++------------ crates/gpui2/src/platform/test/platform.rs | 7 +++---- 4 files changed, 17 insertions(+), 30 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index d23d6e3d9d15003ab3236a6d491143f25d1f48de..0715ace9eaf8ca0de7fabcb635264e361fc09dad 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -1052,16 +1052,7 @@ impl AppContext { } pub fn set_menus(&mut self, menus: Vec) { - if let Some(active_window) = self.active_window() { - active_window - .update(self, |_, cx| { - cx.platform - .set_menus(menus, Some(&cx.window.current_frame.dispatch_tree)); - }) - .ok(); - } else { - self.platform.set_menus(menus, None); - } + self.platform.set_menus(menus, &self.keymap.lock()); } pub fn dispatch_action(&mut self, action: &dyn Action) { diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 7bcd91a5e07dbc03a52fedd6837b9e06c542c6f4..66cf7c14efb95ad520083122a900f9ff900c7e94 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -6,8 +6,8 @@ mod mac; mod test; use crate::{ - point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, DispatchTree, - Font, FontId, FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, + point, size, Action, AnyWindowHandle, BackgroundExecutor, Bounds, DevicePixels, Font, FontId, + FontMetrics, FontRun, ForegroundExecutor, GlobalPixels, GlyphId, InputEvent, Keymap, LineLayout, Pixels, Point, RenderGlyphParams, RenderImageParams, RenderSvgParams, Result, Scene, SharedString, Size, TaskLabel, }; @@ -92,7 +92,7 @@ pub(crate) trait Platform: 'static { fn on_reopen(&self, callback: Box); fn on_event(&self, callback: Box bool>); - fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>); + fn set_menus(&self, menus: Vec, keymap: &Keymap); fn on_app_menu_action(&self, callback: Box); fn on_will_open_app_menu(&self, callback: Box); fn on_validate_app_menu_command(&self, callback: Box bool>); diff --git a/crates/gpui2/src/platform/mac/platform.rs b/crates/gpui2/src/platform/mac/platform.rs index 8a5ee676f7c9e76364e6aea27a9b064a917cca36..2deea545e164b3445dcd6388048eaf97b66e9102 100644 --- a/crates/gpui2/src/platform/mac/platform.rs +++ b/crates/gpui2/src/platform/mac/platform.rs @@ -1,7 +1,7 @@ use super::{events::key_to_native, BoolExt}; use crate::{ - Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, - DisplayId, ForegroundExecutor, InputEvent, MacDispatcher, MacDisplay, MacDisplayLinker, + Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, + ForegroundExecutor, InputEvent, Keymap, MacDispatcher, MacDisplay, MacDisplayLinker, MacTextSystem, MacWindow, Menu, MenuItem, PathPromptOptions, Platform, PlatformDisplay, PlatformTextSystem, PlatformWindow, Result, SemanticVersion, VideoTimestamp, WindowOptions, }; @@ -206,7 +206,7 @@ impl MacPlatform { menus: Vec, delegate: id, actions: &mut Vec>, - dispatch_tree: Option<&DispatchTree>, + keymap: &Keymap, ) -> id { let application_menu = NSMenu::new(nil).autorelease(); application_menu.setDelegate_(delegate); @@ -217,7 +217,7 @@ impl MacPlatform { menu.setDelegate_(delegate); for item_config in menu_config.items { - menu.addItem_(self.create_menu_item(item_config, delegate, actions, dispatch_tree)); + menu.addItem_(self.create_menu_item(item_config, delegate, actions, keymap)); } let menu_item = NSMenuItem::new(nil).autorelease(); @@ -238,7 +238,7 @@ impl MacPlatform { item: MenuItem, delegate: id, actions: &mut Vec>, - dispatch_tree: Option<&DispatchTree>, + keymap: &Keymap, ) -> id { match item { MenuItem::Separator => NSMenuItem::separatorItem(nil), @@ -247,11 +247,8 @@ impl MacPlatform { action, os_action, } => { - let bindings = dispatch_tree - .map(|tree| tree.bindings_for_action(action.as_ref(), &tree.context_stack)) - .unwrap_or_default(); - let keystrokes = bindings - .iter() + let keystrokes = keymap + .bindings_for_action(action.type_id()) .find(|binding| binding.action().partial_eq(action.as_ref())) .map(|binding| binding.keystrokes()); @@ -343,7 +340,7 @@ impl MacPlatform { let submenu = NSMenu::new(nil).autorelease(); submenu.setDelegate_(delegate); for item in items { - submenu.addItem_(self.create_menu_item(item, delegate, actions, dispatch_tree)); + submenu.addItem_(self.create_menu_item(item, delegate, actions, keymap)); } item.setSubmenu_(submenu); item.setTitle_(ns_string(name)); @@ -691,12 +688,12 @@ impl Platform for MacPlatform { } } - fn set_menus(&self, menus: Vec, dispatch_tree: Option<&DispatchTree>) { + fn set_menus(&self, menus: Vec, keymap: &Keymap) { unsafe { let app: id = msg_send![APP_CLASS, sharedApplication]; let mut state = self.0.lock(); let actions = &mut state.menu_actions; - app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, dispatch_tree)); + app.setMainMenu_(self.create_menu_bar(menus, app.delegate(), actions, keymap)); } } diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index c76796b5225f9e67ac51fe0898ffef975fc02cad..10fd9f0ff38e7abb57a11d201c9745371cef3f49 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -1,7 +1,6 @@ use crate::{ - AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DispatchTree, DisplayId, - ForegroundExecutor, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, - WindowOptions, + AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId, ForegroundExecutor, + Keymap, Platform, PlatformDisplay, PlatformTextSystem, TestDisplay, TestWindow, WindowOptions, }; use anyhow::{anyhow, Result}; use collections::VecDeque; @@ -213,7 +212,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn set_menus(&self, _menus: Vec, _dispatch_tree: Option<&DispatchTree>) { + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) { unimplemented!() } From 886ec79d587ffa00b0d96f14f5114dac8c2e8124 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:45:59 +0100 Subject: [PATCH 56/90] Make TestPlatform::set_menus a no-op --- crates/gpui2/src/platform/test/platform.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index 10fd9f0ff38e7abb57a11d201c9745371cef3f49..edbe39480994a9425f733ea5a5ed967f63535f11 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -212,9 +212,7 @@ impl Platform for TestPlatform { unimplemented!() } - fn set_menus(&self, _menus: Vec, _keymap: &Keymap) { - unimplemented!() - } + fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} fn on_app_menu_action(&self, _callback: Box) { unimplemented!() From 80c8fd1f4c09ec049ae2b96fa0aa584e3d70fed2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 11:54:59 -0500 Subject: [PATCH 57/90] Fix toolbar not appearing for initial pane (#3512) This PR fixes an issues where the toolbar would not appear for the center pane when Zed2 initially loads. We resolved this by adding a call to initialize the center pane when the workspace is initialized Due to changes in the way subscriptions work we can on longer observe an event that is emitted in the same event cycle in which the subscription is created. Because of this we need to explicitly initialize the center pane, as it won't get performed by the subscription. Release Notes: - N/A --------- Co-authored-by: Antonio --- crates/zed2/src/zed2.rs | 64 ++++++++++++++++++++++------------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 5f2099154cc8f298baedceeee3ab282fe8eca079..daa25b8eb95948c2aa281831529c20bd07c6d2d8 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -11,7 +11,7 @@ use breadcrumbs::Breadcrumbs; use collections::VecDeque; use editor::{Editor, MultiBuffer}; use gpui::{ - actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, + actions, point, px, AppContext, Context, FocusableView, PromptLevel, TitlebarOptions, View, ViewContext, VisualContext, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; @@ -30,6 +30,7 @@ use util::{ ResultExt, }; use uuid::Uuid; +use workspace::Pane; use workspace::{ create_and_open_local_file, dock::PanelHandle, notifications::simple_message_notification::MessageNotification, open_new, AppState, NewFile, @@ -92,37 +93,12 @@ pub fn build_window_options( pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { cx.observe_new_views(move |workspace: &mut Workspace, cx| { let workspace_handle = cx.view().clone(); + let center_pane = workspace.active_pane().clone(); + initialize_pane(workspace, ¢er_pane, cx); cx.subscribe(&workspace_handle, { move |workspace, _, event, cx| { if let workspace::Event::PaneAdded(pane) = event { - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace)); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); - toolbar.add_item(buffer_search_bar.clone(), cx); - - let quick_action_bar = cx - .build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); - toolbar.add_item(quick_action_bar, cx); - let diagnostic_editor_controls = - cx.build_view(|_| diagnostics::ToolbarControls::new()); - // toolbar.add_item(diagnostic_editor_controls, cx); - // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - // toolbar.add_item(project_search_bar, cx); - // let submit_feedback_button = - // cx.add_view(|_| SubmitFeedbackButton::new()); - // toolbar.add_item(submit_feedback_button, cx); - // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); - // toolbar.add_item(feedback_info_text, cx); - // let lsp_log_item = - // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); - // toolbar.add_item(lsp_log_item, cx); - // let syntax_tree_item = cx - // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); - // toolbar.add_item(syntax_tree_item, cx); - }) - }); + initialize_pane(workspace, pane, cx); } } }) @@ -434,6 +410,36 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .detach(); } +fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewContext) { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.build_view(|_| Breadcrumbs::new(workspace)); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.build_view(search::BufferSearchBar::new); + toolbar.add_item(buffer_search_bar.clone(), cx); + + let quick_action_bar = + cx.build_view(|_| QuickActionBar::new(buffer_search_bar, workspace)); + toolbar.add_item(quick_action_bar, cx); + let diagnostic_editor_controls = cx.build_view(|_| diagnostics::ToolbarControls::new()); + // toolbar.add_item(diagnostic_editor_controls, cx); + // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + // toolbar.add_item(project_search_bar, cx); + // let submit_feedback_button = + // cx.add_view(|_| SubmitFeedbackButton::new()); + // toolbar.add_item(submit_feedback_button, cx); + // let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + // toolbar.add_item(feedback_info_text, cx); + // let lsp_log_item = + // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); + // toolbar.add_item(lsp_log_item, cx); + // let syntax_tree_item = cx + // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); + // toolbar.add_item(syntax_tree_item, cx); + }) + }); +} + fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { use std::fmt::Write as _; From 80f315106d88beb7c9bca8afe49dc2760d67a8ee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:56:05 +0100 Subject: [PATCH 58/90] Add key context to ConversationEditor Co-Authored-By: Marshall --- crates/assistant2/src/assistant_panel.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 2a589afc4f519fa51e2567d55851443f1a538703..b926cb51efd9445498da5c63790117f040e2af13 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2474,6 +2474,7 @@ impl Render for ConversationEditor { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() + .key_context("ConversationEditor") .size_full() .relative() .capture_action(cx.listener(ConversationEditor::cancel_last_assist)) From 5e558e2a58e6b1e38df3df6480a2efd2288f3ec0 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 17:57:18 +0100 Subject: [PATCH 59/90] Make more menu-related platform methods no-ops --- crates/gpui2/src/platform/test/platform.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/gpui2/src/platform/test/platform.rs b/crates/gpui2/src/platform/test/platform.rs index edbe39480994a9425f733ea5a5ed967f63535f11..876120b62626657541fabd1b790e857e16bd5865 100644 --- a/crates/gpui2/src/platform/test/platform.rs +++ b/crates/gpui2/src/platform/test/platform.rs @@ -214,17 +214,11 @@ impl Platform for TestPlatform { fn set_menus(&self, _menus: Vec, _keymap: &Keymap) {} - fn on_app_menu_action(&self, _callback: Box) { - unimplemented!() - } + fn on_app_menu_action(&self, _callback: Box) {} - fn on_will_open_app_menu(&self, _callback: Box) { - unimplemented!() - } + fn on_will_open_app_menu(&self, _callback: Box) {} - fn on_validate_app_menu_command(&self, _callback: Box bool>) { - unimplemented!() - } + fn on_validate_app_menu_command(&self, _callback: Box bool>) {} fn os_name(&self) -> &'static str { "test" From 2aee3e3192cd7fd682a288bc27444ef5954089c7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 18:02:45 +0100 Subject: [PATCH 60/90] Make `Node::context` optional as well This was an oversight in d09dfe0. Co-Authored-By: Marshall --- crates/gpui2/src/key_dispatch.rs | 10 +++++----- crates/gpui2/src/window.rs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index 80e662ad3ef6477b4cd0d925b2852957a4c04d00..7b8d506d03b2feb5c349838625a51e9d2d27b4b7 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -28,7 +28,7 @@ pub(crate) struct DispatchTree { pub(crate) struct DispatchNode { pub key_listeners: SmallVec<[KeyListener; 2]>, pub action_listeners: SmallVec<[DispatchActionListener; 16]>, - pub context: KeyContext, + pub context: Option, parent: Option, } @@ -70,14 +70,14 @@ impl DispatchTree { }); self.node_stack.push(node_id); if let Some(context) = context { - self.active_node().context = context.clone(); + self.active_node().context = Some(context.clone()); self.context_stack.push(context); } } pub fn pop_node(&mut self) { let node_id = self.node_stack.pop().unwrap(); - if !self.nodes[node_id.0].context.is_empty() { + if self.nodes[node_id.0].context.is_some() { self.context_stack.pop(); } } @@ -95,8 +95,8 @@ impl DispatchTree { self.context_stack.clear(); for node_id in dispatch_path { let node = self.node(node_id); - if !node.context.is_empty() { - self.context_stack.push(node.context.clone()); + if let Some(context) = node.context.clone() { + self.context_stack.push(context); } if let Some((context_stack, matcher)) = old_tree diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 8995d04b64de4abac4653e228691f3784eb89a6e..2f4708984355661c90764e1a00daf31084259486 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1393,8 +1393,8 @@ impl<'a> WindowContext<'a> { for node_id in &dispatch_path { let node = self.window.current_frame.dispatch_tree.node(*node_id); - if !node.context.is_empty() { - context_stack.push(node.context.clone()); + if let Some(context) = node.context.clone() { + context_stack.push(context); } for key_listener in node.key_listeners.clone() { @@ -1418,7 +1418,7 @@ impl<'a> WindowContext<'a> { // Match keystrokes let node = self.window.current_frame.dispatch_tree.node(*node_id); - if !node.context.is_empty() { + if node.context.is_some() { if let Some(key_down_event) = event.downcast_ref::() { if let Some(found) = self .window @@ -1563,7 +1563,7 @@ impl<'a> WindowContext<'a> { let context_stack = dispatch_tree .dispatch_path(node_id) .into_iter() - .map(|node_id| dispatch_tree.node(node_id).context.clone()) + .filter_map(|node_id| dispatch_tree.node(node_id).context.clone()) .collect(); dispatch_tree.bindings_for_action(action, &context_stack) } From f6a7a6c4d4df8ec1cfcc7fc299a494c1718c26d6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 12:03:59 -0500 Subject: [PATCH 61/90] v0.117.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 004214c9c85f0642e579c60672b40b04626ed1f8..0d4bb71f9826e4f35f096ef3661afaceb60f4fff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11706,7 +11706,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.116.0" +version = "0.117.0" dependencies = [ "activity_indicator", "ai", diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 245bb4cd5895e6f72641241b193987780bdec768..6d9cb3c7502bc539cedc26d45c97fde5e5b7099f 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.116.0" +version = "0.117.0" publish = false [lib] From 5ae20607833b27ef22b6566c10516aba022fde95 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Wed, 6 Dec 2023 12:34:47 -0500 Subject: [PATCH 62/90] collab 0.30.0 --- Cargo.lock | 2 +- crates/collab/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d4bb71f9826e4f35f096ef3661afaceb60f4fff..f67b3dd0ae0fdd1bcec98b4f59e407021ef01451 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,7 +1702,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.29.1" +version = "0.30.0" dependencies = [ "anyhow", "async-trait", diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index 33c3c14ddd6552fc53a873694bcd1541f945ebe0..50491704c9792a241f10b2c2ac3dbcf146b77c61 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.29.1" +version = "0.30.0" publish = false [[bin]] From e4884f1d7600740e97c9ea46ac2416d51ab444fb Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 18:39:50 +0100 Subject: [PATCH 63/90] Move assistant actions to the top of the crate Co-Authored-By: Marshall --- crates/assistant2/src/assistant.rs | 15 +++++++++- crates/assistant2/src/assistant_panel.rs | 35 ++++++++---------------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/crates/assistant2/src/assistant.rs b/crates/assistant2/src/assistant.rs index 91d61a19f98b4cf4a61257a68d8f7212c6a33586..910eeda9e115d853cfe2a7ccaea041245050484e 100644 --- a/crates/assistant2/src/assistant.rs +++ b/crates/assistant2/src/assistant.rs @@ -12,12 +12,25 @@ use chrono::{DateTime, Local}; use collections::HashMap; use fs::Fs; use futures::StreamExt; -use gpui::AppContext; +use gpui::{actions, AppContext}; use regex::Regex; use serde::{Deserialize, Serialize}; use std::{cmp::Reverse, ffi::OsStr, path::PathBuf, sync::Arc}; use util::paths::CONVERSATIONS_DIR; +actions!( + NewConversation, + Assist, + Split, + CycleMessageRole, + QuoteSelection, + ToggleFocus, + ResetKey, + InlineAssist, + ToggleIncludeConversation, + ToggleRetrieveContext, +); + #[derive( Copy, Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Hash, Serialize, Deserialize, )] diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index b926cb51efd9445498da5c63790117f040e2af13..cea7199759477ee441a7f4d4427255199268fc44 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2,8 +2,9 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel}, codegen::{self, Codegen, CodegenKind}, prompts::generate_content_prompt, - MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata, - SavedMessage, + Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus, + NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, + SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext, }; use ai::{ @@ -28,12 +29,12 @@ use editor::{ use fs::Fs; use futures::StreamExt; use gpui::{ - actions, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, - AsyncWindowContext, ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, - FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, - ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString, - StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, - View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext, + div, point, relative, rems, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, + ClipboardItem, Context, Div, EventEmitter, FocusHandle, Focusable, FocusableView, FontStyle, + FontWeight, HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, + ParentElement, Pixels, PromptLevel, Render, SharedString, StatefulInteractiveElement, Styled, + Subscription, Task, TextStyle, UniformListScrollHandle, View, ViewContext, VisualContext, + WeakModel, WeakView, WhiteSpace, WindowContext, }; use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _}; use project::Project; @@ -51,10 +52,9 @@ use std::{ sync::Arc, time::{Duration, Instant}, }; -use theme::{ActiveTheme, ThemeSettings}; +use theme::ThemeSettings; use ui::{ - h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton, - IconElement, Label, Selectable, Tooltip, + h_stack, prelude::*, v_stack, Button, ButtonLike, Icon, IconButton, IconElement, Label, Tooltip, }; use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt}; use uuid::Uuid; @@ -64,19 +64,6 @@ use workspace::{ Save, Toast, ToggleZoom, Toolbar, Workspace, }; -actions!( - NewConversation, - Assist, - Split, - CycleMessageRole, - QuoteSelection, - ToggleFocus, - ResetKey, - InlineAssist, - ToggleIncludeConversation, - ToggleRetrieveContext, -); - pub fn init(cx: &mut AppContext) { AssistantSettings::register(cx); cx.observe_new_views( From cc9eff89f5226c419a9e697660208b964c1cf544 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 19:12:01 +0100 Subject: [PATCH 64/90] Use a handler instead of an action for clicks This prevents dispatching actions on buttons that were not the target of the click. Co-Authored-By: Marshall --- crates/assistant2/src/assistant_panel.rs | 8 ++- .../quick_action_bar2/src/quick_action_bar.rs | 19 ++++--- crates/search2/src/buffer_search.rs | 50 +++++++++---------- .../ui2/src/components/button/icon_button.rs | 6 +-- crates/workspace2/src/dock.rs | 5 +- 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index cea7199759477ee441a7f4d4427255199268fc44..202f8a2092152158341cc5aaf0752fa74ea23b2c 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2574,7 +2574,9 @@ impl Render for InlineAssistant { .w(measurements.gutter_width) .child( IconButton::new("include_conversation", Icon::Ai) - .action(Box::new(ToggleIncludeConversation)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_include_conversation(&ToggleIncludeConversation, cx) + })) .selected(self.include_conversation) .tooltip(|cx| { Tooltip::for_action( @@ -2587,7 +2589,9 @@ impl Render for InlineAssistant { .children(if SemanticIndex::enabled(cx) { Some( IconButton::new("retrieve_context", Icon::MagnifyingGlass) - .action(Box::new(ToggleRetrieveContext)) + .on_click(cx.listener(|this, _, cx| { + this.toggle_retrieve_context(&ToggleRetrieveContext, cx) + })) .selected(self.retrieve_context) .tooltip(|cx| { Tooltip::for_action( diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 3232de08adea814fcf33e7fc54b598a93bf18c25..3686ace2fb0b73a530a98681a5639c3a1bfc71e4 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -2,8 +2,8 @@ use editor::Editor; use gpui::{ - Action, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, - Styled, Subscription, View, ViewContext, WeakView, + Action, ClickEvent, Div, ElementId, EventEmitter, InteractiveElement, ParentElement, Render, + Stateful, Styled, Subscription, View, ViewContext, WeakView, }; use search::BufferSearchBar; use ui::{prelude::*, ButtonSize, ButtonStyle, Icon, IconButton, IconSize, Tooltip}; @@ -41,19 +41,24 @@ impl Render for QuickActionBar { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let buffer_search_bar = self.buffer_search_bar.clone(); let search_button = QuickActionBarButton::new( "toggle buffer search", Icon::MagnifyingGlass, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(search::buffer_search::Deploy { focus: false }), "Buffer Search", + move |_, cx| { + buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx)); + }, ); let assistant_button = QuickActionBarButton::new( - "toggle inline assitant", + "toggle inline assistant", Icon::MagicWand, false, Box::new(gpui::NoAction), "Inline assistant", + |_, _cx| todo!(), ); h_stack() .id("quick action bar") @@ -154,6 +159,7 @@ struct QuickActionBarButton { action: Box, tooltip: SharedString, tooltip_meta: Option, + on_click: Box, } impl QuickActionBarButton { @@ -163,6 +169,7 @@ impl QuickActionBarButton { toggled: bool, action: Box, tooltip: impl Into, + on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static, ) -> Self { Self { id: id.into(), @@ -171,6 +178,7 @@ impl QuickActionBarButton { action, tooltip: tooltip.into(), tooltip_meta: None, + on_click: Box::new(on_click), } } @@ -201,10 +209,7 @@ impl RenderOnce for QuickActionBarButton { Tooltip::for_action(tooltip.clone(), &*action, cx) } }) - .on_click({ - let action = self.action.boxed_clone(); - move |_, cx| cx.dispatch_action(action.boxed_clone()) - }) + .on_click(move |event, cx| (self.on_click)(event, cx)) } } diff --git a/crates/search2/src/buffer_search.rs b/crates/search2/src/buffer_search.rs index b9fa36ef34e4b7413043a8d1c76357c55953270a..da32f51194cc68d869a76ba8b88023f5450b2844 100644 --- a/crates/search2/src/buffer_search.rs +++ b/crates/search2/src/buffer_search.rs @@ -18,7 +18,7 @@ use project::search::SearchQuery; use serde::Deserialize; use std::{any::Any, sync::Arc}; -use ui::{h_stack, Icon, IconButton, IconElement}; +use ui::{h_stack, Clickable, Icon, IconButton, IconElement}; use util::ResultExt; use workspace::{ item::ItemHandle, @@ -161,16 +161,6 @@ impl Render for BufferSearchBar { Some(ui::Label::new(message)) }); - let nav_button_for_direction = |icon, direction| { - render_nav_button( - icon, - self.active_match_index.is_some(), - cx.listener(move |this, _, cx| match direction { - Direction::Prev => this.select_prev_match(&Default::default(), cx), - Direction::Next => this.select_next_match(&Default::default(), cx), - }), - ) - }; let should_show_replace_input = self.replace_enabled && supported_options.replacement; let replace_all = should_show_replace_input .then(|| super::render_replace_button(ReplaceAll, ui::Icon::ReplaceAll)); @@ -237,15 +227,21 @@ impl Render for BufferSearchBar { h_stack() .gap_0p5() .flex_none() - .child(self.render_action_button()) + .child(self.render_action_button(cx)) .children(match_count) - .child(nav_button_for_direction( + .child(render_nav_button( ui::Icon::ChevronLeft, - Direction::Prev, + self.active_match_index.is_some(), + cx.listener(move |this, _, cx| { + this.select_prev_match(&Default::default(), cx); + }), )) - .child(nav_button_for_direction( + .child(render_nav_button( ui::Icon::ChevronRight, - Direction::Next, + self.active_match_index.is_some(), + cx.listener(move |this, _, cx| { + this.select_next_match(&Default::default(), cx); + }), )), ) } @@ -317,13 +313,7 @@ impl BufferSearchBar { pane.update(cx, |this, cx| { this.toolbar().update(cx, |this, cx| { if let Some(search_bar) = this.item_of_type::() { - search_bar.update(cx, |this, cx| { - if this.is_dismissed() { - this.show(cx); - } else { - this.dismiss(&Dismiss, cx); - } - }); + search_bar.update(cx, |this, cx| this.toggle(cx)); return; } let view = cx.build_view(|cx| BufferSearchBar::new(cx)); @@ -487,6 +477,14 @@ impl BufferSearchBar { false } + pub fn toggle(&mut self, cx: &mut ViewContext) { + if self.is_dismissed() { + self.show(cx); + } else { + self.dismiss(&Dismiss, cx); + } + } + pub fn show(&mut self, cx: &mut ViewContext) -> bool { if self.active_searchable_item.is_none() { return false; @@ -588,12 +586,14 @@ impl BufferSearchBar { self.update_matches(cx) } - fn render_action_button(&self) -> impl IntoElement { + fn render_action_button(&self, cx: &mut ViewContext) -> impl IntoElement { // let tooltip_style = theme.tooltip.clone(); // let style = theme.search.action_button.clone(); - IconButton::new(0, ui::Icon::SelectAll).action(Box::new(SelectAllMatches)) + IconButton::new("select-all", ui::Icon::SelectAll).on_click(cx.listener(|this, _, cx| { + this.select_all_matches(&SelectAllMatches, cx); + })) } pub fn activate_search_mode(&mut self, mode: SearchMode, cx: &mut ViewContext) { diff --git a/crates/ui2/src/components/button/icon_button.rs b/crates/ui2/src/components/button/icon_button.rs index 94431ef642e08840034ff7f2e4025be5f220daac..f49120e90c1c1afea3857152ee6d286cb7051596 100644 --- a/crates/ui2/src/components/button/icon_button.rs +++ b/crates/ui2/src/components/button/icon_button.rs @@ -1,4 +1,4 @@ -use gpui::{Action, AnyView, DefiniteLength}; +use gpui::{AnyView, DefiniteLength}; use crate::prelude::*; use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, IconSize}; @@ -39,10 +39,6 @@ impl IconButton { self.selected_icon = icon.into(); self } - - pub fn action(self, action: Box) -> Self { - self.on_click(move |_event, cx| cx.dispatch_action(action.boxed_clone())) - } } impl Disableable for IconButton { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index abcf5c49bc929c5954c3e40557e419e4ecaca702..a0a90293d603a3afde61dda2c4ca59ec5fa273a1 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -724,7 +724,10 @@ impl Render for PanelButtons { .trigger( IconButton::new(name, icon) .selected(is_active_button) - .action(action.boxed_clone()) + .on_click({ + let action = action.boxed_clone(); + move |_, cx| cx.dispatch_action(action.boxed_clone()) + }) .tooltip(move |cx| { Tooltip::for_action(tooltip.clone(), &*action, cx) }), From 7e2ff63270e6779350fdbf7b0f6d49a3e7668612 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 6 Dec 2023 19:15:09 +0100 Subject: [PATCH 65/90] Paint blocks on top of the editor This ensures blocks get mouse events before the editor beneath them. Co-Authored-By: Marshall --- crates/editor2/src/element.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ab11f5ffb5427d39905675756be5d9996e984fc1..d7badd4ab5cc080a9f5bb2d58ef586416539632f 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2835,8 +2835,10 @@ impl Element for EditorElement { self.paint_text(text_bounds, &mut layout, cx); if !layout.blocks.is_empty() { - cx.with_element_id(Some("editor_blocks"), |cx| { - self.paint_blocks(bounds, &mut layout, cx); + cx.with_z_index(1, |cx| { + cx.with_element_id(Some("editor_blocks"), |cx| { + self.paint_blocks(bounds, &mut layout, cx); + }) }) } }); From fdd64832e75f34a1277505274b5b01953a867acf Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 13:52:38 -0500 Subject: [PATCH 66/90] Update inline assist styles --- crates/assistant2/src/assistant_panel.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/assistant2/src/assistant_panel.rs b/crates/assistant2/src/assistant_panel.rs index 202f8a2092152158341cc5aaf0752fa74ea23b2c..e7c9d4c21a30ac98f4cd2e03fbd965cc74d1e40c 100644 --- a/crates/assistant2/src/assistant_panel.rs +++ b/crates/assistant2/src/assistant_panel.rs @@ -2562,6 +2562,10 @@ impl Render for InlineAssistant { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let measurements = self.measurements.get(); h_stack() + .w_full() + .py_2() + .border_y_1() + .border_color(cx.theme().colors().border) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::toggle_include_conversation)) @@ -2617,7 +2621,8 @@ impl Render for InlineAssistant { }), ) .child( - div() + h_stack() + .w_full() .ml(measurements.anchor_x - measurements.gutter_width) .child(self.render_prompt_editor(cx)), ) From 147c99f1a750a035a66cad8b5b571f499eb131d2 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 14:28:31 -0500 Subject: [PATCH 67/90] Fix layout for terminal tabs (#3514) This PR fixes the layout for terminal tabs. We need to use an `h_stack` here to get the icon and the label to position themselves next to each other instead of one on top of the other. Release Notes: - N/A --- Cargo.lock | 1 + crates/terminal_view2/Cargo.toml | 1 + crates/terminal_view2/src/terminal_view.rs | 12 ++++++------ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9d6fb877fa10a256c6c746451cfcd58e9112136..4b5af36a19b8fcf1049e7d762db189a50730359a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9531,6 +9531,7 @@ dependencies = [ "terminal2", "theme2", "thiserror", + "ui2", "util", "workspace2", ] diff --git a/crates/terminal_view2/Cargo.toml b/crates/terminal_view2/Cargo.toml index 12e2c06504fad5adc7e502c4225a28c9bf985f30..9654bed7f56f236d0dc57196e1e25a9452c507e3 100644 --- a/crates/terminal_view2/Cargo.toml +++ b/crates/terminal_view2/Cargo.toml @@ -21,6 +21,7 @@ workspace = { package = "workspace2", path = "../workspace2" } db = { package = "db2", path = "../db2" } procinfo = { git = "https://github.com/zed-industries/wezterm", rev = "5cd757e5f2eb039ed0c6bb6512223e69d5efc64d", default-features = false } terminal = { package = "terminal2", path = "../terminal2" } +ui = { package = "ui2", path = "../ui2" } smallvec.workspace = true smol.workspace = true mio-extras = "2.0.6" diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 570b37ba098b86c159b7acce1e6941402336ec97..49da703addd1e4380341fd8590589610706cd536 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,10 +9,9 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, Action, AnyElement, AppContext, Div, Element, EventEmitter, FocusEvent, - FocusHandle, Focusable, FocusableElement, FocusableView, InputHandler, InteractiveElement, - KeyDownEvent, Keystroke, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Render, - SharedString, Styled, Task, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle, + Focusable, FocusableElement, FocusableView, InputHandler, KeyDownEvent, Keystroke, Model, + MouseButton, MouseDownEvent, Pixels, Render, Task, View, VisualContext, WeakView, }; use language::Bias; use persistence::TERMINAL_DB; @@ -25,13 +24,13 @@ use terminal::{ terminal_settings::{TerminalBlink, TerminalSettings, WorkingDirectory}, Event, MaybeNavigationTarget, Terminal, }; +use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ item::{BreadcrumbText, Item, ItemEvent}, notifications::NotifyResultExt, register_deserializable_item, searchable::{SearchEvent, SearchOptions, SearchableItem}, - ui::{ContextMenu, Icon, IconElement, Label}, CloseActiveItem, NewCenterTerminal, Pane, ToolbarItemLocation, Workspace, WorkspaceId, }; @@ -745,7 +744,8 @@ impl Item for TerminalView { fn tab_content(&self, _detail: Option, cx: &WindowContext) -> AnyElement { let title = self.terminal().read(cx).title(); - div() + h_stack() + .gap_2() .child(IconElement::new(Icon::Terminal)) .child(Label::new(title)) .into_any() From ac07e230faf4f3d84fd176775908045c39a6d4b9 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 6 Dec 2023 12:28:44 -0700 Subject: [PATCH 68/90] Document geometry --- crates/editor2/src/editor_tests.rs | 56 +- crates/editor2/src/element.rs | 4 +- crates/gpui2/src/elements/overlay.rs | 2 +- crates/gpui2/src/geometry.rs | 1247 ++++++++++++++++++++- crates/gpui2/src/platform/test/display.rs | 2 +- crates/gpui2/src/platform/test/window.rs | 4 +- crates/gpui2/src/style.rs | 2 +- crates/gpui2/src/window.rs | 2 +- 8 files changed, 1285 insertions(+), 34 deletions(-) diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index 571cbd84bb179be0b1562dd07f5c7a0114e1b8e4..9ffa3e523bff4aafb840abfe0c699f62776707e6 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -345,7 +345,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -356,7 +361,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { ); editor.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -368,7 +378,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { editor.update(cx, |view, cx| { view.end_selection(cx); - view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -380,7 +395,12 @@ fn test_selection_with_mouse(cx: &mut TestAppContext) { editor.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 3), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 0), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(0, 0), + 0, + gpui::Point::::default(), + cx, + ); }); assert_eq!( @@ -423,7 +443,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { }); view.update(cx, |view, cx| { - view.update_selection(DisplayPoint::new(3, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(3, 3), + 0, + gpui::Point::::default(), + cx, + ); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -432,7 +457,12 @@ fn test_canceling_pending_selection(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.cancel(&Cancel, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); assert_eq!( view.selections.display_ranges(cx), [DisplayPoint::new(2, 2)..DisplayPoint::new(3, 3)] @@ -643,11 +673,21 @@ fn test_cancel(cx: &mut TestAppContext) { view.update(cx, |view, cx| { view.begin_selection(DisplayPoint::new(3, 4), false, 1, cx); - view.update_selection(DisplayPoint::new(1, 1), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(1, 1), + 0, + gpui::Point::::default(), + cx, + ); view.end_selection(cx); view.begin_selection(DisplayPoint::new(0, 1), true, 1, cx); - view.update_selection(DisplayPoint::new(0, 3), 0, gpui::Point::::zero(), cx); + view.update_selection( + DisplayPoint::new(0, 3), + 0, + gpui::Point::::default(), + cx, + ); view.end_selection(cx); assert_eq!( view.selections.display_ranges(cx), diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index ab11f5ffb5427d39905675756be5d9996e984fc1..7c7d7472f2ad22ecdd592c9b79fc62ca6a0b56ed 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -485,7 +485,7 @@ impl EditorElement { let modifiers = event.modifiers; if editor.has_pending_selection() && event.pressed_button == Some(MouseButton::Left) { let point_for_position = position_map.point_for_position(text_bounds, event.position); - let mut scroll_delta = gpui::Point::::zero(); + let mut scroll_delta = gpui::Point::::default(); let vertical_margin = position_map.line_height.min(text_bounds.size.height / 3.0); let top = text_bounds.origin.y + vertical_margin; let bottom = text_bounds.lower_left().y - vertical_margin; @@ -511,7 +511,7 @@ impl EditorElement { position: point_for_position.previous_valid, goal_column: point_for_position.exact_unclipped.column(), scroll_position: (position_map.snapshot.scroll_position() + scroll_delta) - .clamp(&gpui::Point::zero(), &position_map.scroll_max), + .clamp(&gpui::Point::default(), &position_map.scroll_max), }, cx, ); diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs index 764bdfabcd6695d3ee0b4dd71cc51e472567c09e..7d4b90963760abb1ad9df260ad418fba47819bb4 100644 --- a/crates/gpui2/src/elements/overlay.rs +++ b/crates/gpui2/src/elements/overlay.rs @@ -102,7 +102,7 @@ impl Element for Overlay { let mut desired = self.anchor_corner.get_bounds(origin, size); let limits = Bounds { - origin: Point::zero(), + origin: Point::default(), size: cx.viewport_size(), }; diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 20afd2d288b29bc8953c41c79389b9331a80244b..50f680f493cae5ee2c2d634c982ad30b357f5880 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -8,6 +8,18 @@ use std::{ ops::{Add, Div, Mul, MulAssign, Sub}, }; +/// Describes a location in a 2D cartesian coordinate space. +/// +/// It holds two public fields, `x` and `y`, which represent the coordinates in the space. +/// The type `T` for the coordinates can be any type that implements `Default`, `Clone`, and `Debug`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Point; +/// let point = Point { x: 10, y: 20 }; +/// println!("{:?}", point); // Outputs: Point { x: 10, y: 20 } +/// ``` #[derive(Refineable, Default, Add, AddAssign, Sub, SubAssign, Copy, Debug, PartialEq, Eq, Hash)] #[refineable(Debug)] #[repr(C)] @@ -16,19 +28,66 @@ pub struct Point { pub y: T, } +/// Constructs a new `Point` with the given x and y coordinates. +/// +/// # Arguments +/// +/// * `x` - The x coordinate of the point. +/// * `y` - The y coordinate of the point. +/// +/// # Returns +/// +/// Returns a `Point` with the specified coordinates. +/// +/// # Examples +/// +/// ``` +/// # use zed::Point; +/// let p = point(10, 20); +/// assert_eq!(p.x, 10); +/// assert_eq!(p.y, 20); +/// ``` pub fn point(x: T, y: T) -> Point { Point { x, y } } impl Point { + /// Creates a new `Point` with the specified `x` and `y` coordinates. + /// + /// # Arguments + /// + /// * `x` - The horizontal coordinate of the point. + /// * `y` - The vertical coordinate of the point. + /// + /// # Examples + /// + /// ``` + /// let p = Point::new(10, 20); + /// assert_eq!(p.x, 10); + /// assert_eq!(p.y, 20); + /// ``` pub const fn new(x: T, y: T) -> Self { Self { x, y } } - pub fn zero() -> Self { - Self::new(T::default(), T::default()) - } - + /// Transforms the point to a `Point` by applying the given function to both coordinates. + /// + /// This method allows for converting a `Point` to a `Point` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `x` + /// and `y` coordinates, resulting in a new point of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p = Point { x: 3, y: 4 }; + /// let p_float = p.map(|coord| coord as f32); + /// assert_eq!(p_float, Point { x: 3.0, y: 4.0 }); + /// ``` pub fn map(&self, f: impl Fn(T) -> U) -> Point { Point { x: f(self.x.clone()), @@ -38,6 +97,21 @@ impl Point { } impl Point { + /// Scales the point by a given factor, which is typically derived from the resolution + /// of a target display to ensure proper sizing of UI elements. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to both the x and y coordinates. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Point, Pixels, ScaledPixels}; + /// let p = Point { x: Pixels(10.0), y: Pixels(20.0) }; + /// let scaled_p = p.scale(1.5); + /// assert_eq!(scaled_p, Point { x: ScaledPixels(15.0), y: ScaledPixels(30.0) }); + /// ``` pub fn scale(&self, factor: f32) -> Point { Point { x: self.x.scale(factor), @@ -45,6 +119,16 @@ impl Point { } } + /// Calculates the Euclidean distance from the origin (0, 0) to this point. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// # use zed::Pixels; + /// let p = Point { x: Pixels(3.0), y: Pixels(4.0) }; + /// assert_eq!(p.magnitude(), 5.0); + /// ``` pub fn magnitude(&self) -> f64 { ((self.x.0.powi(2) + self.y.0.powi(2)) as f64).sqrt() } @@ -95,14 +179,29 @@ impl Point where T: PartialOrd + Clone + Default + Debug, { + /// Returns a new point with the maximum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let max_point = p1.max(&p2); + /// assert_eq!(max_point, Point { x: 5, y: 7 }); + /// ``` pub fn max(&self, other: &Self) -> Self { Point { - x: if self.x >= other.x { + x: if self.x > other.x { self.x.clone() } else { other.x.clone() }, - y: if self.y >= other.y { + y: if self.y > other.y { self.y.clone() } else { other.y.clone() @@ -110,6 +209,21 @@ where } } + /// Returns a new point with the minimum values of each dimension from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Point` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p1 = Point { x: 3, y: 7 }; + /// let p2 = Point { x: 5, y: 2 }; + /// let min_point = p1.min(&p2); + /// assert_eq!(min_point, Point { x: 3, y: 2 }); + /// ``` pub fn min(&self, other: &Self) -> Self { Point { x: if self.x <= other.x { @@ -125,6 +239,32 @@ where } } + /// Clamps the point to a specified range. + /// + /// Given a minimum point and a maximum point, this method constrains the current point + /// such that its coordinates do not exceed the range defined by the minimum and maximum points. + /// If the current point's coordinates are less than the minimum, they are set to the minimum. + /// If they are greater than the maximum, they are set to the maximum. + /// + /// # Arguments + /// + /// * `min` - A reference to a `Point` representing the minimum allowable coordinates. + /// * `max` - A reference to a `Point` representing the maximum allowable coordinates. + /// + /// # Examples + /// + /// ``` + /// # use zed::Point; + /// let p = Point { x: 10, y: 20 }; + /// let min = Point { x: 0, y: 5 }; + /// let max = Point { x: 15, y: 25 }; + /// let clamped_p = p.clamp(&min, &max); + /// assert_eq!(clamped_p, Point { x: 10, y: 20 }); + /// + /// let p_out_of_bounds = Point { x: -5, y: 30 }; + /// let clamped_p_out_of_bounds = p_out_of_bounds.clamp(&min, &max); + /// assert_eq!(clamped_p_out_of_bounds, Point { x: 0, y: 25 }); + /// ``` pub fn clamp(&self, min: &Self, max: &Self) -> Self { self.max(min).min(max) } @@ -139,6 +279,10 @@ impl Clone for Point { } } +/// A structure representing a two-dimensional size with width and height in a given unit. +/// +/// This struct is generic over the type `T`, which can be any type that implements `Clone`, `Default`, and `Debug`. +/// It is commonly used to specify dimensions for elements in a UI, such as a window or element. #[derive(Refineable, Default, Clone, Copy, PartialEq, Div, Hash, Serialize, Deserialize)] #[refineable(Debug)] #[repr(C)] @@ -147,6 +291,21 @@ pub struct Size { pub height: T, } +/// Constructs a new `Size` with the provided width and height. +/// +/// # Arguments +/// +/// * `width` - The width component of the `Size`. +/// * `height` - The height component of the `Size`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Size; +/// let my_size = size(10, 20); +/// assert_eq!(my_size.width, 10); +/// assert_eq!(my_size.height, 20); +/// ``` pub fn size(width: T, height: T) -> Size where T: Clone + Default + Debug, @@ -158,6 +317,24 @@ impl Size where T: Clone + Default + Debug, { + /// Applies a function to the width and height of the size, producing a new `Size`. + /// + /// This method allows for converting a `Size` to a `Size` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to both the `width` + /// and `height`, resulting in a new size of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let my_size = Size { width: 10, height: 20 }; + /// let my_new_size = my_size.map(|dimension| dimension as f32 * 1.5); + /// assert_eq!(my_new_size, Size { width: 15.0, height: 30.0 }); + /// ``` pub fn map(&self, f: impl Fn(T) -> U) -> Size where U: Clone + Default + Debug, @@ -170,6 +347,24 @@ where } impl Size { + /// Scales the size by a given factor. + /// + /// This method multiplies both the width and height by the provided scaling factor, + /// resulting in a new `Size` that is proportionally larger or smaller + /// depending on the factor. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the width and height. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Size, Pixels, ScaledPixels}; + /// let size = Size { width: Pixels(100.0), height: Pixels(50.0) }; + /// let scaled_size = size.scale(2.0); + /// assert_eq!(scaled_size, Size { width: ScaledPixels(200.0), height: ScaledPixels(100.0) }); + /// ``` pub fn scale(&self, factor: f32) -> Size { Size { width: self.width.scale(factor), @@ -182,6 +377,21 @@ impl Size where T: PartialOrd + Clone + Default + Debug, { + /// Returns a new `Size` with the maximum width and height from `self` and `other`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Size` to compare with `self`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Size; + /// let size1 = Size { width: 30, height: 40 }; + /// let size2 = Size { width: 50, height: 20 }; + /// let max_size = size1.max(&size2); + /// assert_eq!(max_size, Size { width: 50, height: 40 }); + /// ``` pub fn max(&self, other: &Self) -> Self { Size { width: if self.width >= other.width { @@ -286,6 +496,14 @@ impl From> for Size { } impl Size { + /// Returns a `Size` with both width and height set to fill the available space. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Definite(DefiniteLength::Fraction(1.0))`, + /// which represents 100% of the available space in both dimensions. + /// + /// # Returns + /// + /// A `Size` that will fill the available space when used in a layout. pub fn full() -> Self { Self { width: relative(1.).into(), @@ -294,16 +512,16 @@ impl Size { } } -impl Size { - pub fn zero() -> Self { - Self { - width: px(0.).into(), - height: px(0.).into(), - } - } -} - impl Size { + /// Returns a `Size` with both width and height set to `auto`, which allows the layout engine to determine the size. + /// + /// This function creates a `Size` instance where both the width and height are set to `Length::Auto`, + /// indicating that their size should be computed based on the layout context, such as the content size or + /// available space. + /// + /// # Returns + /// + /// A `Size` with width and height set to `Length::Auto`. pub fn auto() -> Self { Self { width: Length::Auto, @@ -312,6 +530,23 @@ impl Size { } } +/// Represents a rectangular area in a 2D space with an origin point and a size. +/// +/// The `Bounds` struct is generic over a type `T` which represents the type of the coordinate system. +/// The origin is represented as a `Point` which defines the upper-left corner of the rectangle, +/// and the size is represented as a `Size` which defines the width and height of the rectangle. +/// +/// # Examples +/// +/// ``` +/// # use zed::{Bounds, Point, Size}; +/// let origin = Point { x: 0, y: 0 }; +/// let size = Size { width: 10, height: 20 }; +/// let bounds = Bounds::new(origin, size); +/// +/// assert_eq!(bounds.origin, origin); +/// assert_eq!(bounds.size, size); +/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -324,6 +559,33 @@ impl Bounds where T: Clone + Debug + Sub + Default, { + /// Constructs a `Bounds` from two corner points: the upper-left and lower-right corners. + /// + /// This function calculates the origin and size of the `Bounds` based on the provided corner points. + /// The origin is set to the upper-left corner, and the size is determined by the difference between + /// the x and y coordinates of the lower-right and upper-left points. + /// + /// # Arguments + /// + /// * `upper_left` - A `Point` representing the upper-left corner of the rectangle. + /// * `lower_right` - A `Point` representing the lower-right corner of the rectangle. + /// + /// # Returns + /// + /// Returns a `Bounds` that encompasses the area defined by the two corner points. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point}; + /// let upper_left = Point { x: 0, y: 0 }; + /// let lower_right = Point { x: 10, y: 10 }; + /// let bounds = Bounds::from_corners(upper_left, lower_right); + /// + /// assert_eq!(bounds.origin, upper_left); + /// assert_eq!(bounds.size.width, 10); + /// assert_eq!(bounds.size.height, 10); + /// ``` pub fn from_corners(upper_left: Point, lower_right: Point) -> Self { let origin = Point { x: upper_left.x.clone(), @@ -336,6 +598,16 @@ where Bounds { origin, size } } + /// Creates a new `Bounds` with the specified origin and size. + /// + /// # Arguments + /// + /// * `origin` - A `Point` representing the origin of the bounds. + /// * `size` - A `Size` representing the size of the bounds. + /// + /// # Returns + /// + /// Returns a `Bounds` that has the given origin and size. pub fn new(origin: Point, size: Size) -> Self { Bounds { origin, size } } @@ -345,6 +617,39 @@ impl Bounds where T: Clone + Debug + PartialOrd + Add + Sub + Default + Half, { + /// Checks if this `Bounds` intersects with another `Bounds`. + /// + /// Two `Bounds` instances intersect if they overlap in the 2D space they occupy. + /// This method checks if there is any overlapping area between the two bounds. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to check for intersection with. + /// + /// # Returns + /// + /// Returns `true` if there is any intersection between the two bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds3 = Bounds { + /// origin: Point { x: 20, y: 20 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// + /// assert_eq!(bounds1.intersects(&bounds2), true); // Overlapping bounds + /// assert_eq!(bounds1.intersects(&bounds3), false); // Non-overlapping bounds + /// ``` pub fn intersects(&self, other: &Bounds) -> bool { let my_lower_right = self.lower_right(); let their_lower_right = other.lower_right(); @@ -355,6 +660,32 @@ where && my_lower_right.y > other.origin.y } + /// Dilates the bounds by a specified amount in all directions. + /// + /// This method expands the bounds by the given `amount`, increasing the size + /// and adjusting the origin so that the bounds grow outwards equally in all directions. + /// The resulting bounds will have its width and height increased by twice the `amount` + /// (since it grows in both directions), and the origin will be moved by `-amount` + /// in both the x and y directions. + /// + /// # Arguments + /// + /// * `amount` - The amount by which to dilate the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let mut bounds = Bounds { + /// origin: Point { x: 10, y: 10 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// bounds.dilate(5); + /// assert_eq!(bounds, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` pub fn dilate(&mut self, amount: T) { self.origin.x = self.origin.x.clone() - amount.clone(); self.origin.y = self.origin.y.clone() - amount.clone(); @@ -363,6 +694,27 @@ where self.size.height = self.size.height.clone() + double_amount; } + /// Returns the center point of the bounds. + /// + /// Calculates the center by taking the origin's x and y coordinates and adding half the width and height + /// of the bounds, respectively. The center is represented as a `Point` where `T` is the type of the + /// coordinate system. + /// + /// # Returns + /// + /// A `Point` representing the center of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let center = bounds.center(); + /// assert_eq!(center, Point { x: 5, y: 10 }); + /// ``` pub fn center(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone().half(), @@ -372,12 +724,78 @@ where } impl + Sub> Bounds { + /// Calculates the intersection of two `Bounds` objects. + /// + /// This method computes the overlapping region of two `Bounds`. If the bounds do not intersect, + /// the resulting `Bounds` will have a size with width and height of zero. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to intersect with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the intersection area. If there is no intersection, + /// the returned `Bounds` will have a size with width and height of zero. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let intersection = bounds1.intersect(&bounds2); + /// + /// assert_eq!(intersection, Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 5, height: 5 }, + /// }); + /// ``` pub fn intersect(&self, other: &Self) -> Self { let upper_left = self.origin.max(&other.origin); let lower_right = self.lower_right().min(&other.lower_right()); Self::from_corners(upper_left, lower_right) } + /// Computes the union of two `Bounds`. + /// + /// This method calculates the smallest `Bounds` that contains both the current `Bounds` and the `other` `Bounds`. + /// The resulting `Bounds` will have an origin that is the minimum of the origins of the two `Bounds`, + /// and a size that encompasses the furthest extents of both `Bounds`. + /// + /// # Arguments + /// + /// * `other` - A reference to another `Bounds` to create a union with. + /// + /// # Returns + /// + /// Returns a `Bounds` representing the union of the two `Bounds`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds1 = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let bounds2 = Bounds { + /// origin: Point { x: 5, y: 5 }, + /// size: Size { width: 15, height: 15 }, + /// }; + /// let union_bounds = bounds1.union(&bounds2); + /// + /// assert_eq!(union_bounds, Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 20, height: 20 }, + /// }); + /// ``` pub fn union(&self, other: &Self) -> Self { let top_left = self.origin.min(&other.origin); let bottom_right = self.lower_right().max(&other.lower_right()); @@ -432,22 +850,59 @@ impl Bounds where T: Add + Clone + Default + Debug, { + /// Returns the top edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the top edge of the bounds. pub fn top(&self) -> T { self.origin.y.clone() } + /// Returns the bottom edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the y-coordinate of the bottom edge of the bounds. pub fn bottom(&self) -> T { self.origin.y.clone() + self.size.height.clone() } + /// Returns the left edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the left edge of the bounds. pub fn left(&self) -> T { self.origin.x.clone() } + /// Returns the right edge of the bounds. + /// + /// # Returns + /// + /// A value of type `T` representing the x-coordinate of the right edge of the bounds. pub fn right(&self) -> T { self.origin.x.clone() + self.size.width.clone() } + /// Returns the upper-right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the upper-right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let upper_right = bounds.upper_right(); + /// assert_eq!(upper_right, Point { x: 10, y: 0 }); + /// ``` pub fn upper_right(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone(), @@ -455,6 +910,23 @@ where } } + /// Returns the lower-right corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the lower-right corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let lower_right = bounds.lower_right(); + /// assert_eq!(lower_right, Point { x: 10, y: 20 }); + /// ``` pub fn lower_right(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone(), @@ -462,6 +934,23 @@ where } } + /// Returns the lower-left corner point of the bounds. + /// + /// # Returns + /// + /// A `Point` representing the lower-left corner of the bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 20 }, + /// }; + /// let lower_left = bounds.lower_left(); + /// assert_eq!(lower_left, Point { x: 0, y: 20 }); + /// ``` pub fn lower_left(&self) -> Point { Point { x: self.origin.x.clone(), @@ -474,6 +963,35 @@ impl Bounds where T: Add + PartialOrd + Clone + Default + Debug, { + /// Checks if the given point is within the bounds. + /// + /// This method determines whether a point lies inside the rectangle defined by the bounds, + /// including the edges. The point is considered inside if its x-coordinate is greater than + /// or equal to the left edge and less than or equal to the right edge, and its y-coordinate + /// is greater than or equal to the top edge and less than or equal to the bottom edge of the bounds. + /// + /// # Arguments + /// + /// * `point` - A reference to a `Point` that represents the point to check. + /// + /// # Returns + /// + /// Returns `true` if the point is within the bounds, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Point, Bounds}; + /// let bounds = Bounds { + /// origin: Point { x: 0, y: 0 }, + /// size: Size { width: 10, height: 10 }, + /// }; + /// let inside_point = Point { x: 5, y: 5 }; + /// let outside_point = Point { x: 15, y: 15 }; + /// + /// assert!(bounds.contains_point(&inside_point)); + /// assert!(!bounds.contains_point(&outside_point)); + /// ``` pub fn contains_point(&self, point: &Point) -> bool { point.x >= self.origin.x && point.x <= self.origin.x.clone() + self.size.width.clone() @@ -481,6 +999,34 @@ where && point.y <= self.origin.y.clone() + self.size.height.clone() } + /// Applies a function to the origin and size of the bounds, producing a new `Bounds`. + /// + /// This method allows for converting a `Bounds` to a `Bounds` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to the `origin` and + /// `size` fields, resulting in new bounds of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Bounds` with the origin and size mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size}; + /// let bounds = Bounds { + /// origin: Point { x: 10.0, y: 10.0 }, + /// size: Size { width: 10.0, height: 20.0 }, + /// }; + /// let new_bounds = bounds.map(|value| value as f64 * 1.5); + /// + /// assert_eq!(new_bounds, Bounds { + /// origin: Point { x: 15.0, y: 15.0 }, + /// size: Size { width: 15.0, height: 30.0 }, + /// }); pub fn map(&self, f: impl Fn(T) -> U) -> Bounds where U: Clone + Default + Debug, @@ -493,6 +1039,36 @@ where } impl Bounds { + /// Scales the bounds by a given factor, typically used to adjust for display scaling. + /// + /// This method multiplies the origin and size of the bounds by the provided scaling factor, + /// resulting in a new `Bounds` that is proportionally larger or smaller + /// depending on the scaling factor. This can be used to ensure that the bounds are properly + /// scaled for different display densities. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to the origin and size, typically the display's scaling factor. + /// + /// # Returns + /// + /// Returns a new `Bounds` that represents the scaled bounds. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Bounds, Point, Size, Pixels}; + /// let bounds = Bounds { + /// origin: Point { x: Pixels(10.0), y: Pixels(20.0) }, + /// size: Size { width: Pixels(30.0), height: Pixels(40.0) }, + /// }; + /// let display_scale_factor = 2.0; + /// let scaled_bounds = bounds.scale(display_scale_factor); + /// assert_eq!(scaled_bounds, Bounds { + /// origin: Point { x: ScaledPixels(20.0), y: ScaledPixels(40.0) }, + /// size: Size { width: ScaledPixels(60.0), height: ScaledPixels(80.0) }, + /// }); + /// ``` pub fn scale(&self, factor: f32) -> Bounds { Bounds { origin: self.origin.scale(factor), @@ -503,6 +1079,26 @@ impl Bounds { impl Copy for Bounds {} +/// Represents the edges of a box in a 2D space, such as padding or margin. +/// +/// Each field represents the size of the edge on one side of the box: `top`, `right`, `bottom`, and `left`. +/// +/// # Examples +/// +/// ``` +/// # use zed::Edges; +/// let edges = Edges { +/// top: 10.0, +/// right: 20.0, +/// bottom: 30.0, +/// left: 40.0, +/// }; +/// +/// assert_eq!(edges.top, 10.0); +/// assert_eq!(edges.right, 20.0); +/// assert_eq!(edges.bottom, 30.0); +/// assert_eq!(edges.left, 40.0); +/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -545,6 +1141,30 @@ where impl Copy for Edges {} impl Edges { + /// Constructs `Edges` where all sides are set to the same specified value. + /// + /// This function creates an `Edges` instance with the `top`, `right`, `bottom`, and `left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform edges around a box, + /// such as padding or margin with the same size on all sides. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four sides of the edges. + /// + /// # Returns + /// + /// An `Edges` instance with all sides set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let uniform_edges = Edges::all(10.0); + /// assert_eq!(uniform_edges.top, 10.0); + /// assert_eq!(uniform_edges.right, 10.0); + /// assert_eq!(uniform_edges.bottom, 10.0); + /// assert_eq!(uniform_edges.left, 10.0); + /// ``` pub fn all(value: T) -> Self { Self { top: value.clone(), @@ -554,6 +1174,28 @@ impl Edges { } } + /// Applies a function to each field of the `Edges`, producing a new `Edges`. + /// + /// This method allows for converting an `Edges` to an `Edges` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top`, `right`, `bottom`, `left`), resulting in new edges of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Edges` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let edges = Edges { top: 10, right: 20, bottom: 30, left: 40 }; + /// let edges_float = edges.map(|&value| value as f32 * 1.1); + /// assert_eq!(edges_float, Edges { top: 11.0, right: 22.0, bottom: 33.0, left: 44.0 }); + /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> Edges where U: Clone + Default + Debug, @@ -566,6 +1208,33 @@ impl Edges { } } + /// Checks if any of the edges satisfy a given predicate. + /// + /// This method applies a predicate function to each field of the `Edges` and returns `true` if any field satisfies the predicate. + /// + /// # Arguments + /// + /// * `predicate` - A closure that takes a reference to a value of type `T` and returns a `bool`. + /// + /// # Returns + /// + /// Returns `true` if the predicate returns `true` for any of the edge values, `false` otherwise. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let edges = Edges { + /// top: 10, + /// right: 0, + /// bottom: 5, + /// left: 0, + /// }; + /// + /// assert!(edges.any(|value| *value == 0)); + /// assert!(edges.any(|value| *value > 0)); + /// assert!(!edges.any(|value| *value > 10)); + /// ``` pub fn any bool>(&self, predicate: F) -> bool { predicate(&self.top) || predicate(&self.right) @@ -575,6 +1244,24 @@ impl Edges { } impl Edges { + /// Sets the edges of the `Edges` struct to `auto`, which is a special value that allows the layout engine to automatically determine the size of the edges. + /// + /// This is typically used in layout contexts where the exact size of the edges is not important, or when the size should be calculated based on the content or container. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to `Length::Auto`. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let auto_edges = Edges::auto(); + /// assert_eq!(auto_edges.top, Length::Auto); + /// assert_eq!(auto_edges.right, Length::Auto); + /// assert_eq!(auto_edges.bottom, Length::Auto); + /// assert_eq!(auto_edges.left, Length::Auto); + /// ``` pub fn auto() -> Self { Self { top: Length::Auto, @@ -584,6 +1271,25 @@ impl Edges { } } + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.right, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.bottom, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// assert_eq!(no_edges.left, Length::Definite(DefiniteLength::from(Pixels(0.)))); + /// ``` pub fn zero() -> Self { Self { top: px(0.).into(), @@ -595,6 +1301,25 @@ impl Edges { } impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.right, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.bottom, DefiniteLength::from(zed::px(0.))); + /// assert_eq!(no_edges.left, DefiniteLength::from(zed::px(0.))); + /// ``` pub fn zero() -> Self { Self { top: px(0.).into(), @@ -604,6 +1329,42 @@ impl Edges { } } + /// Converts the `DefiniteLength` to `Pixels` based on the parent size and the REM size. + /// + /// This method allows for a `DefiniteLength` value to be converted into pixels, taking into account + /// the size of the parent element (for percentage-based lengths) and the size of a rem unit (for rem-based lengths). + /// + /// # Arguments + /// + /// * `parent_size` - `Size` representing the size of the parent element. + /// * `rem_size` - `Pixels` representing the size of one REM unit. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, DefiniteLength, px, AbsoluteLength, Size}; + /// let edges = Edges { + /// top: DefiniteLength::Absolute(AbsoluteLength::Pixels(px(10.0))), + /// right: DefiniteLength::Fraction(0.5), + /// bottom: DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))), + /// left: DefiniteLength::Fraction(0.25), + /// }; + /// let parent_size = Size { + /// width: AbsoluteLength::Pixels(px(200.0)), + /// height: AbsoluteLength::Pixels(px(100.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(parent_size, rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Absolute length in pixels + /// assert_eq!(edges_in_pixels.right, px(100.0)); // 50% of parent width + /// assert_eq!(edges_in_pixels.bottom, px(32.0)); // 2 rems + /// assert_eq!(edges_in_pixels.left, px(50.0)); // 25% of parent width + /// ``` pub fn to_pixels(&self, parent_size: Size, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(parent_size.height, rem_size), @@ -615,6 +1376,25 @@ impl Edges { } impl Edges { + /// Sets the edges of the `Edges` struct to zero, which means no size or thickness. + /// + /// This is typically used when you want to specify that a box (like a padding or margin area) + /// should have no edges, effectively making it non-existent or invisible in layout calculations. + /// + /// # Returns + /// + /// Returns an `Edges` with all edges set to zero length. + /// + /// # Examples + /// + /// ``` + /// # use zed::Edges; + /// let no_edges = Edges::zero(); + /// assert_eq!(no_edges.top, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.right, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.bottom, AbsoluteLength::Pixels(Pixels(0.0))); + /// assert_eq!(no_edges.left, AbsoluteLength::Pixels(Pixels(0.0))); + /// ``` pub fn zero() -> Self { Self { top: px(0.).into(), @@ -624,6 +1404,37 @@ impl Edges { } } + /// Converts the `AbsoluteLength` to `Pixels` based on the `rem_size`. + /// + /// If the `AbsoluteLength` is already in pixels, it simply returns the corresponding `Pixels` value. + /// If the `AbsoluteLength` is in rems, it multiplies the number of rems by the `rem_size` to convert it to pixels. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem unit in pixels. + /// + /// # Returns + /// + /// Returns an `Edges` representing the edges with lengths converted to pixels. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, AbsoluteLength, Pixels, px}; + /// let edges = Edges { + /// top: AbsoluteLength::Pixels(px(10.0)), + /// right: AbsoluteLength::Rems(rems(1.0)), + /// bottom: AbsoluteLength::Pixels(px(20.0)), + /// left: AbsoluteLength::Rems(rems(2.0)), + /// }; + /// let rem_size = px(16.0); + /// let edges_in_pixels = edges.to_pixels(rem_size); + /// + /// assert_eq!(edges_in_pixels.top, px(10.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.right, px(16.0)); // 1 rem converted to pixels + /// assert_eq!(edges_in_pixels.bottom, px(20.0)); // Already in pixels + /// assert_eq!(edges_in_pixels.left, px(32.0)); // 2 rems converted to pixels + /// ``` pub fn to_pixels(&self, rem_size: Pixels) -> Edges { Edges { top: self.top.to_pixels(rem_size), @@ -635,6 +1446,34 @@ impl Edges { } impl Edges { + /// Scales the `Edges` by a given factor, returning `Edges`. + /// + /// This method is typically used for adjusting the edge sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each edge. + /// + /// # Returns + /// + /// Returns a new `Edges` where each edge is the result of scaling the original edge by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Edges, Pixels}; + /// let edges = Edges { + /// top: Pixels(10.0), + /// right: Pixels(20.0), + /// bottom: Pixels(30.0), + /// left: Pixels(40.0), + /// }; + /// let scaled_edges = edges.scale(2.0); + /// assert_eq!(scaled_edges.top, ScaledPixels(20.0)); + /// assert_eq!(scaled_edges.right, ScaledPixels(40.0)); + /// assert_eq!(scaled_edges.bottom, ScaledPixels(60.0)); + /// assert_eq!(scaled_edges.left, ScaledPixels(80.0)); + /// ``` pub fn scale(&self, factor: f32) -> Edges { Edges { top: self.top.scale(factor), @@ -645,6 +1484,10 @@ impl Edges { } } +/// Represents the corners of a box in a 2D space, such as border radius. +/// +/// Each field represents the size of the corner on one side of the box: `top_left`, `top_right`, `bottom_right`, and `bottom_left`. +/// ``` #[derive(Refineable, Clone, Default, Debug, Eq, PartialEq)] #[refineable(Debug)] #[repr(C)] @@ -659,6 +1502,30 @@ impl Corners where T: Clone + Default + Debug, { + /// Constructs `Corners` where all sides are set to the same specified value. + /// + /// This function creates a `Corners` instance with the `top_left`, `top_right`, `bottom_right`, and `bottom_left` fields all initialized + /// to the same value provided as an argument. This is useful when you want to have uniform corners around a box, + /// such as a uniform border radius on a rectangle. + /// + /// # Arguments + /// + /// * `value` - The value to set for all four corners. + /// + /// # Returns + /// + /// An `Corners` instance with all corners set to the given value. + /// + /// # Examples + /// + /// ``` + /// # use zed::Corners; + /// let uniform_corners = Corners::all(5.0); + /// assert_eq!(uniform_corners.top_left, 5.0); + /// assert_eq!(uniform_corners.top_right, 5.0); + /// assert_eq!(uniform_corners.bottom_right, 5.0); + /// assert_eq!(uniform_corners.bottom_left, 5.0); + /// ``` pub fn all(value: T) -> Self { Self { top_left: value.clone(), @@ -670,6 +1537,42 @@ where } impl Corners { + /// Converts the `AbsoluteLength` to `Pixels` based on the provided size and rem size, ensuring the resulting + /// `Pixels` do not exceed half of the maximum of the provided size's width and height. + /// + /// This method is particularly useful when dealing with corner radii, where the radius in pixels should not + /// exceed half the size of the box it applies to, to avoid the corners overlapping. + /// + /// # Arguments + /// + /// * `size` - The `Size` against which the maximum allowable radius is determined. + /// * `rem_size` - The size of one REM unit in pixels, used for conversion if the `AbsoluteLength` is in REMs. + /// + /// # Returns + /// + /// Returns a `Corners` instance with each corner's length converted to pixels and clamped to the + /// maximum allowable radius based on the provided size. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, AbsoluteLength, Pixels, Size}; + /// let corners = Corners { + /// top_left: AbsoluteLength::Pixels(Pixels(15.0)), + /// top_right: AbsoluteLength::Rems(Rems(1.0)), + /// bottom_right: AbsoluteLength::Pixels(Pixels(20.0)), + /// bottom_left: AbsoluteLength::Rems(Rems(2.0)), + /// }; + /// let size = Size { width: Pixels(100.0), height: Pixels(50.0) }; + /// let rem_size = Pixels(16.0); + /// let corners_in_pixels = corners.to_pixels(size, rem_size); + /// + /// // The resulting corners should not exceed half the size of the smallest dimension (50.0 / 2.0 = 25.0). + /// assert_eq!(corners_in_pixels.top_left, Pixels(15.0)); + /// assert_eq!(corners_in_pixels.top_right, Pixels(16.0)); // 1 rem converted to pixels + /// assert_eq!(corners_in_pixels.bottom_right, Pixels(20.0).min(Pixels(25.0))); // Clamped to 25.0 + /// assert_eq!(corners_in_pixels.bottom_left, Pixels(32.0).min(Pixels(25.0))); // 2 rems converted to pixels and clamped + /// ``` pub fn to_pixels(&self, size: Size, rem_size: Pixels) -> Corners { let max = size.width.max(size.height) / 2.; Corners { @@ -682,6 +1585,34 @@ impl Corners { } impl Corners { + /// Scales the `Corners` by a given factor, returning `Corners`. + /// + /// This method is typically used for adjusting the corner sizes for different display densities or scaling factors. + /// + /// # Arguments + /// + /// * `factor` - The scaling factor to apply to each corner. + /// + /// # Returns + /// + /// Returns a new `Corners` where each corner is the result of scaling the original corner by the given factor. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, Pixels}; + /// let corners = Corners { + /// top_left: Pixels(10.0), + /// top_right: Pixels(20.0), + /// bottom_right: Pixels(30.0), + /// bottom_left: Pixels(40.0), + /// }; + /// let scaled_corners = corners.scale(2.0); + /// assert_eq!(scaled_corners.top_left, ScaledPixels(20.0)); + /// assert_eq!(scaled_corners.top_right, ScaledPixels(40.0)); + /// assert_eq!(scaled_corners.bottom_right, ScaledPixels(60.0)); + /// assert_eq!(scaled_corners.bottom_left, ScaledPixels(80.0)); + /// ``` pub fn scale(&self, factor: f32) -> Corners { Corners { top_left: self.top_left.scale(factor), @@ -693,6 +1624,38 @@ impl Corners { } impl Corners { + /// Applies a function to each field of the `Corners`, producing a new `Corners`. + /// + /// This method allows for converting a `Corners` to a `Corners` by specifying a closure + /// that defines how to convert between the two types. The closure is applied to each field + /// (`top_left`, `top_right`, `bottom_right`, `bottom_left`), resulting in new corners of the desired type. + /// + /// # Arguments + /// + /// * `f` - A closure that takes a reference to a value of type `T` and returns a value of type `U`. + /// + /// # Returns + /// + /// Returns a new `Corners` with each field mapped by the provided function. + /// + /// # Examples + /// + /// ``` + /// # use zed::{Corners, Pixels}; + /// let corners = Corners { + /// top_left: Pixels(10.0), + /// top_right: Pixels(20.0), + /// bottom_right: Pixels(30.0), + /// bottom_left: Pixels(40.0), + /// }; + /// let corners_in_rems = corners.map(|&px| Rems(px.0 / 16.0)); + /// assert_eq!(corners_in_rems, Corners { + /// top_left: Rems(0.625), + /// top_right: Rems(1.25), + /// bottom_right: Rems(1.875), + /// bottom_left: Rems(2.5), + /// }); + /// ``` pub fn map(&self, f: impl Fn(&T) -> U) -> Corners where U: Clone + Default + Debug, @@ -737,6 +1700,28 @@ where impl Copy for Corners where T: Copy + Clone + Default + Debug {} +/// Represents a length in pixels, the base unit of measurement in the UI framework. +/// +/// `Pixels` is a value type that represents an absolute length in pixels, which is used +/// for specifying sizes, positions, and distances in the UI. It is the fundamental unit +/// of measurement for all visual elements and layout calculations. +/// +/// The inner value is an `f32`, allowing for sub-pixel precision which can be useful for +/// anti-aliasing and animations. However, when applied to actual pixel grids, the value +/// is typically rounded to the nearest integer. +/// +/// # Examples +/// +/// ``` +/// use zed::Pixels; +/// +/// // Define a length of 10 pixels +/// let length = Pixels(10.0); +/// +/// // Define a length and scale it by a factor of 2 +/// let scaled_length = length.scale(2.0); +/// assert_eq!(scaled_length, Pixels(20.0)); +/// ``` #[derive( Clone, Copy, @@ -815,29 +1800,68 @@ impl MulAssign for Pixels { } impl Pixels { + /// Represents zero pixels. pub const ZERO: Pixels = Pixels(0.0); + /// The maximum value that can be represented by `Pixels`. pub const MAX: Pixels = Pixels(f32::MAX); + /// Floors the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the floored value. pub fn floor(&self) -> Self { Self(self.0.floor()) } + /// Rounds the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the rounded value. pub fn round(&self) -> Self { Self(self.0.round()) } + /// Returns the ceiling of the `Pixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the ceiling value. pub fn ceil(&self) -> Self { Self(self.0.ceil()) } + /// Scales the `Pixels` value by a given factor, producing `ScaledPixels`. + /// + /// This method is used when adjusting pixel values for display scaling factors, + /// such as high DPI (dots per inch) or Retina displays, where the pixel density is higher and + /// thus requires scaling to maintain visual consistency and readability. + /// + /// The resulting `ScaledPixels` represent the scaled value which can be used for rendering + /// calculations where display scaling is considered. pub fn scale(&self, factor: f32) -> ScaledPixels { ScaledPixels(self.0 * factor) } + /// Raises the `Pixels` value to a given power. + /// + /// # Arguments + /// + /// * `exponent` - The exponent to raise the `Pixels` value by. + /// + /// # Returns + /// + /// Returns a new `Pixels` instance with the value raised to the given exponent. pub fn pow(&self, exponent: f32) -> Self { Self(self.0.powf(exponent)) } + /// Returns the absolute value of the `Pixels`. + /// + /// # Returns + /// + /// A new `Pixels` instance with the absolute value of the original `Pixels`. pub fn abs(&self) -> Self { Self(self.0.abs()) } @@ -925,6 +1949,13 @@ impl From for Pixels { } } +/// Represents physical pixels on the display. +/// +/// `DevicePixels` is a unit of measurement that refers to the actual pixels on a device's screen. +/// This type is used when precise pixel manipulation is required, such as rendering graphics or +/// interfacing with hardware that operates on the pixel level. Unlike logical pixels that may be +/// affected by the device's scale factor, `DevicePixels` always correspond to real pixels on the +/// display. #[derive( Add, AddAssign, Clone, Copy, Default, Div, Eq, Hash, Ord, PartialEq, PartialOrd, Sub, SubAssign, )] @@ -932,6 +1963,28 @@ impl From for Pixels { pub struct DevicePixels(pub(crate) i32); impl DevicePixels { + /// Converts the `DevicePixels` value to the number of bytes needed to represent it in memory. + /// + /// This function is useful when working with graphical data that needs to be stored in a buffer, + /// such as images or framebuffers, where each pixel may be represented by a specific number of bytes. + /// + /// # Arguments + /// + /// * `bytes_per_pixel` - The number of bytes used to represent a single pixel. + /// + /// # Returns + /// + /// The number of bytes required to represent the `DevicePixels` value in memory. + /// + /// # Examples + /// + /// ``` + /// # use zed::DevicePixels; + /// let pixels = DevicePixels(10); // 10 device pixels + /// let bytes_per_pixel = 4; // Assume each pixel is represented by 4 bytes (e.g., RGBA) + /// let total_bytes = pixels.to_bytes(bytes_per_pixel); + /// assert_eq!(total_bytes, 40); // 10 pixels * 4 bytes/pixel = 40 bytes + /// ``` pub fn to_bytes(&self, bytes_per_pixel: u8) -> u32 { self.0 as u32 * bytes_per_pixel as u32 } @@ -991,15 +2044,32 @@ impl From for DevicePixels { } } +/// Represents scaled pixels that take into account the device's scale factor. +/// +/// `ScaledPixels` are used to ensure that UI elements appear at the correct size on devices +/// with different pixel densities. When a device has a higher scale factor (such as Retina displays), +/// a single logical pixel may correspond to multiple physical pixels. By using `ScaledPixels`, +/// dimensions and positions can be specified in a way that scales appropriately across different +/// display resolutions. #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] #[repr(transparent)] pub struct ScaledPixels(pub(crate) f32); impl ScaledPixels { + /// Floors the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the floored value. pub fn floor(&self) -> Self { Self(self.0.floor()) } + /// Rounds the `ScaledPixels` value to the nearest whole number. + /// + /// # Returns + /// + /// Returns a new `ScaledPixels` instance with the rounded value. pub fn ceil(&self) -> Self { Self(self.0.ceil()) } @@ -1031,6 +2101,12 @@ impl From for f64 { } } +/// Represents pixels in a global coordinate space, which can span across multiple displays. +/// +/// `GlobalPixels` is used when dealing with a coordinate system that is not limited to a single +/// display's boundaries. This type is particularly useful in multi-monitor setups where +/// positioning and measurements need to be consistent and relative to a "global" origin point +/// rather than being relative to any individual display. #[derive(Clone, Copy, Default, Add, AddAssign, Sub, SubAssign, Div, PartialEq, PartialOrd)] #[repr(transparent)] pub struct GlobalPixels(pub(crate) f32); @@ -1065,6 +2141,14 @@ impl sqlez::bindable::Bind for GlobalPixels { } } +/// Represents a length in rems, a unit based on the font-size of the window, which can be assigned with [WindowContext::set_rem_size]. +/// +/// Rems are used for defining lengths that are scalable and consistent across different UI elements. +/// The value of `1rem` is typically equal to the font-size of the root element (often the `` element in browsers), +/// making it a flexible unit that adapts to the user's text size preferences. In this framework, `rems` serve a similar +/// purpose, allowing for scalable and accessible design that can adjust to different display settings or user preferences. +/// +/// For example, if the root element's font-size is `16px`, then `1rem` equals `16px`. A length of `2rems` would then be `32px`. #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] pub struct Rems(pub f32); @@ -1082,17 +2166,26 @@ impl Debug for Rems { } } +/// Represents an absolute length in pixels or rems. +/// +/// `AbsoluteLength` can be either a fixed number of pixels, which is an absolute measurement not +/// affected by the current font size, or a number of rems, which is relative to the font size of +/// the root element. It is used for specifying dimensions that are either independent of or +/// related to the typographic scale. #[derive(Clone, Copy, Debug, Neg)] pub enum AbsoluteLength { + /// A length in pixels. Pixels(Pixels), + /// A length in rems. Rems(Rems), } impl AbsoluteLength { + /// Checks if the absolute length is zero. pub fn is_zero(&self) -> bool { match self { - AbsoluteLength::Pixels(px) => px.0 == 0., - AbsoluteLength::Rems(rems) => rems.0 == 0., + AbsoluteLength::Pixels(px) => px.0 == 0.0, + AbsoluteLength::Rems(rems) => rems.0 == 0.0, } } } @@ -1110,6 +2203,27 @@ impl From for AbsoluteLength { } impl AbsoluteLength { + /// Converts an `AbsoluteLength` to `Pixels` based on a given `rem_size`. + /// + /// # Arguments + /// + /// * `rem_size` - The size of one rem in pixels. + /// + /// # Returns + /// + /// Returns the `AbsoluteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{AbsoluteLength, Pixels}; + /// let length_in_pixels = AbsoluteLength::Pixels(Pixels(42.0)); + /// let length_in_rems = AbsoluteLength::Rems(Rems(2.0)); + /// let rem_size = Pixels(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(rem_size), Pixels(42.0)); + /// assert_eq!(length_in_rems.to_pixels(rem_size), Pixels(32.0)); + /// ``` pub fn to_pixels(&self, rem_size: Pixels) -> Pixels { match self { AbsoluteLength::Pixels(pixels) => *pixels, @@ -1125,14 +2239,47 @@ impl Default for AbsoluteLength { } /// A non-auto length that can be defined in pixels, rems, or percent of parent. +/// +/// This enum represents lengths that have a specific value, as opposed to lengths that are automatically +/// determined by the context. It includes absolute lengths in pixels or rems, and relative lengths as a +/// fraction of the parent's size. #[derive(Clone, Copy, Neg)] pub enum DefiniteLength { + /// An absolute length specified in pixels or rems. Absolute(AbsoluteLength), - /// A fraction of the parent's size between 0 and 1. + /// A relative length specified as a fraction of the parent's size, between 0 and 1. Fraction(f32), } impl DefiniteLength { + /// Converts the `DefiniteLength` to `Pixels` based on a given `base_size` and `rem_size`. + /// + /// If the `DefiniteLength` is an absolute length, it will be directly converted to `Pixels`. + /// If it is a fraction, the fraction will be multiplied by the `base_size` to get the length in pixels. + /// + /// # Arguments + /// + /// * `base_size` - The base size in `AbsoluteLength` to which the fraction will be applied. + /// * `rem_size` - The size of one rem in pixels, used to convert rems to pixels. + /// + /// # Returns + /// + /// Returns the `DefiniteLength` as `Pixels`. + /// + /// # Examples + /// + /// ``` + /// # use zed::{DefiniteLength, AbsoluteLength, Pixels, px, rems}; + /// let length_in_pixels = DefiniteLength::Absolute(AbsoluteLength::Pixels(px(42.0))); + /// let length_in_rems = DefiniteLength::Absolute(AbsoluteLength::Rems(rems(2.0))); + /// let length_as_fraction = DefiniteLength::Fraction(0.5); + /// let base_size = AbsoluteLength::Pixels(px(100.0)); + /// let rem_size = px(16.0); + /// + /// assert_eq!(length_in_pixels.to_pixels(base_size, rem_size), Pixels(42.0)); + /// assert_eq!(length_in_rems.to_pixels(base_size, rem_size), Pixels(32.0)); + /// assert_eq!(length_as_fraction.to_pixels(base_size, rem_size), Pixels(50.0)); + /// ``` pub fn to_pixels(&self, base_size: AbsoluteLength, rem_size: Pixels) -> Pixels { match self { DefiniteLength::Absolute(size) => size.to_pixels(rem_size), @@ -1180,7 +2327,9 @@ impl Default for DefiniteLength { /// A length that can be defined in pixels, rems, percent of parent, or auto. #[derive(Clone, Copy)] pub enum Length { + /// A definite length specified either in pixels, rems, or as a fraction of the parent's size. Definite(DefiniteLength), + /// An automatic length that is determined by the context in which it is used. Auto, } @@ -1193,6 +2342,18 @@ impl Debug for Length { } } +/// Constructs a `DefiniteLength` representing a relative fraction of a parent size. +/// +/// This function creates a `DefiniteLength` that is a specified fraction of a parent's dimension. +/// The fraction should be a floating-point number between 0.0 and 1.0, where 1.0 represents 100% of the parent's size. +/// +/// # Arguments +/// +/// * `fraction` - The fraction of the parent's size, between 0.0 and 1.0. +/// +/// # Returns +/// +/// A `DefiniteLength` representing the relative length as a fraction of the parent's size. pub fn relative(fraction: f32) -> DefiniteLength { DefiniteLength::Fraction(fraction).into() } @@ -1202,14 +2363,43 @@ pub fn phi() -> DefiniteLength { relative(1.61803398875) } +/// Constructs a `Rems` value representing a length in rems. +/// +/// # Arguments +/// +/// * `rems` - The number of rems for the length. +/// +/// # Returns +/// +/// A `Rems` representing the specified number of rems. pub fn rems(rems: f32) -> Rems { Rems(rems) } +/// Constructs a `Pixels` value representing a length in pixels. +/// +/// # Arguments +/// +/// * `pixels` - The number of pixels for the length. +/// +/// # Returns +/// +/// A `Pixels` representing the specified number of pixels. pub const fn px(pixels: f32) -> Pixels { Pixels(pixels) } +/// Returns a `Length` representing an automatic length. +/// +/// The `auto` length is often used in layout calculations where the length should be determined +/// by the layout context itself rather than being explicitly set. This is commonly used in CSS +/// for properties like `width`, `height`, `margin`, `padding`, etc., where `auto` can be used +/// to instruct the layout engine to calculate the size based on other factors like the size of the +/// container or the intrinsic size of the content. +/// +/// # Returns +/// +/// A `Length` variant set to `Auto`. pub fn auto() -> Length { Length::Auto } @@ -1250,7 +2440,17 @@ impl From<()> for Length { } } +/// Provides a trait for types that can calculate half of their value. +/// +/// The `Half` trait is used for types that can be evenly divided, returning a new instance of the same type +/// representing half of the original value. This is commonly used for types that represent measurements or sizes, +/// such as lengths or pixels, where halving is a frequent operation during layout calculations or animations. pub trait Half { + /// Returns half of the current value. + /// + /// # Returns + /// + /// A new instance of the implementing type, representing half of the original value. fn half(&self) -> Self; } @@ -1290,7 +2490,18 @@ impl Half for GlobalPixels { } } +/// A trait for checking if a value is zero. +/// +/// This trait provides a method to determine if a value is considered to be zero. +/// It is implemented for various numeric and length-related types where the concept +/// of zero is applicable. This can be useful for comparisons, optimizations, or +/// determining if an operation has a neutral effect. pub trait IsZero { + /// Determines if the value is zero. + /// + /// # Returns + /// + /// Returns `true` if the value is zero, `false` otherwise. fn is_zero(&self) -> bool; } diff --git a/crates/gpui2/src/platform/test/display.rs b/crates/gpui2/src/platform/test/display.rs index 78d75296e66d3fa18f1c7ba6403537369ff013a7..95f1daf8e92fc8bc620a91d3c1aa1ed12818c384 100644 --- a/crates/gpui2/src/platform/test/display.rs +++ b/crates/gpui2/src/platform/test/display.rs @@ -15,7 +15,7 @@ impl TestDisplay { id: DisplayId(1), uuid: uuid::Uuid::new_v4(), bounds: Bounds::from_corners( - Point::zero(), + Point::default(), Point::new(GlobalPixels(1920.), GlobalPixels(1080.)), ), } diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index b1bfebad06745f51899421707d2ead4da57bfcb6..245b36da56d8d585a3f4125a2d0c1d30a27144ef 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -78,7 +78,7 @@ impl PlatformWindow for TestWindow { } fn mouse_position(&self) -> Point { - Point::zero() + Point::default() } fn as_any_mut(&mut self) -> &mut dyn std::any::Any { @@ -223,7 +223,7 @@ impl PlatformAtlas for TestAtlas { }, tile_id: TileId(tile_id), bounds: crate::Bounds { - origin: Point::zero(), + origin: Point::default(), size, }, }, diff --git a/crates/gpui2/src/style.rs b/crates/gpui2/src/style.rs index 9254eaeb85246253180e6ffc59c3f1f12895ae59..5c511b41a4982489f8fcd807d7a21b6767ee99ac 100644 --- a/crates/gpui2/src/style.rs +++ b/crates/gpui2/src/style.rs @@ -385,7 +385,7 @@ impl Default for Style { min_size: Size::auto(), max_size: Size::auto(), aspect_ratio: None, - gap: Size::zero(), + gap: Size::default(), // Aligment align_items: None, align_self: None, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6323eb962f101daa66fcfa8ae1482408507b7670..d8ef5cb674a3d02b54b3e3a30297011be357ce66 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1167,7 +1167,7 @@ impl<'a> WindowContext<'a> { } let available_space = cx.window.viewport_size.map(Into::into); - root_view.draw(Point::zero(), available_space, cx); + root_view.draw(Point::default(), available_space, cx); }) }); From 489c25ac6ab43ab14919fbfd5e4cee4a52e6eb56 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 11:32:05 -0800 Subject: [PATCH 69/90] Put ToggleZoom key binding back into the block with no context --- assets/keymaps/default.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 25fafa755e842359fd882b416f5e33e06c42a202..b8d3711132b1063543e7554b88d014df21460361 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -18,6 +18,7 @@ "escape": "menu::Cancel", "ctrl-c": "menu::Cancel", "cmd-shift-w": "workspace::CloseWindow", + "shift-escape": "workspace::ToggleZoom", "cmd-o": "workspace::Open", "cmd-=": "zed::IncreaseBufferFontSize", "cmd-+": "zed::IncreaseBufferFontSize", @@ -388,7 +389,6 @@ "cmd-n": "workspace::NewFile", "cmd-shift-n": "workspace::NewWindow", "ctrl-`": "terminal_panel::ToggleFocus", - "shift-escape": "workspace::ToggleZoom", "cmd-1": ["workspace::ActivatePane", 0], "cmd-2": ["workspace::ActivatePane", 1], "cmd-3": ["workspace::ActivatePane", 2], From f76e1cfd91a4bbf88fc32fcca750dfcceb7a615d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 21:38:30 +0200 Subject: [PATCH 70/90] Pass proper theme colors for inlays and suggestions --- crates/editor2/src/display_map.rs | 6 +++--- crates/editor2/src/editor.rs | 27 +++++++++++++++++++++++++++ crates/language2/src/highlight_map.rs | 2 -- crates/theme2/src/one_themes.rs | 2 -- crates/theme2/src/styles/syntax.rs | 12 ------------ 5 files changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 1aee04dd0ae02b8d4ea98025be177a82e3801ef7..9cc16933711c221a3605bbeca01ba13931048a7b 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -24,7 +24,7 @@ use lsp::DiagnosticSeverity; use std::{any::TypeId, borrow::Cow, fmt::Debug, num::NonZeroU32, ops::Range, sync::Arc}; use sum_tree::{Bias, TreeMap}; use tab_map::TabMap; -use theme::{SyntaxTheme, Theme}; +use theme::{StatusColors, SyntaxTheme, Theme}; use wrap_map::WrapMap; pub use block_map::{ @@ -513,8 +513,8 @@ impl DisplaySnapshot { self.chunks( display_rows, language_aware, - Some(editor_style.syntax.inlay_style), - Some(editor_style.syntax.suggestion_style), + Some(editor_style.hints_style), + Some(editor_style.suggestions_style), ) .map(|chunk| { let mut highlight_style = chunk diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3994990f6414a6a59582e7bba712623bd88255d4..3dbdbf5e3c400ce4b99c441d60ed44770a0f9164 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -499,6 +499,8 @@ pub struct EditorStyle { pub scrollbar_width: Pixels, pub syntax: Arc, pub diagnostic_style: DiagnosticStyle, + pub hints_style: HighlightStyle, + pub suggestions_style: HighlightStyle, } type CompletionId = usize; @@ -7640,6 +7642,18 @@ impl Editor { .editor_style .diagnostic_style .clone(), + // todo!("what about the rest of the highlight style parts for inlays and suggestions?") + hints_style: HighlightStyle { + color: Some(cx.theme().status().hint), + font_weight: Some(FontWeight::BOLD), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, }, )) .into_any_element() @@ -9302,6 +9316,19 @@ impl Render for Editor { scrollbar_width: px(12.), syntax: cx.theme().syntax().clone(), diagnostic_style: cx.theme().diagnostic_style(), + // TODO kb find `HighlightStyle` usages + // todo!("what about the rest of the highlight style parts?") + hints_style: HighlightStyle { + color: Some(cx.theme().status().hint), + font_weight: Some(FontWeight::BOLD), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, + suggestions_style: HighlightStyle { + color: Some(cx.theme().status().predictive), + fade_out: Some(0.6), + ..HighlightStyle::default() + }, }, ) } diff --git a/crates/language2/src/highlight_map.rs b/crates/language2/src/highlight_map.rs index 8e7a35233cf2e702536241099619cb0bff53459e..270ac259c9d78eff8d36a2ee8c8038f117d33260 100644 --- a/crates/language2/src/highlight_map.rs +++ b/crates/language2/src/highlight_map.rs @@ -95,8 +95,6 @@ mod tests { .iter() .map(|(name, color)| (name.to_string(), (*color).into())) .collect(), - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), }; let capture_names = &[ diff --git a/crates/theme2/src/one_themes.rs b/crates/theme2/src/one_themes.rs index e1fb5f1bed21422f64eb210899b3850f7bb1c6d2..fbcabc0ff3897a9b5156f19af8c2cf08361f33f3 100644 --- a/crates/theme2/src/one_themes.rs +++ b/crates/theme2/src/one_themes.rs @@ -191,8 +191,6 @@ pub(crate) fn one_dark() -> Theme { ("variable.special".into(), red.into()), ("variant".into(), HighlightStyle::default()), ], - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), }), }, } diff --git a/crates/theme2/src/styles/syntax.rs b/crates/theme2/src/styles/syntax.rs index cc73caa6dfca3c920cf79af89eb7d1993d670688..0f35bf60a73aa634bf0953e19f4beea1354131c3 100644 --- a/crates/theme2/src/styles/syntax.rs +++ b/crates/theme2/src/styles/syntax.rs @@ -8,12 +8,6 @@ use crate::{ #[derive(Clone, Default)] pub struct SyntaxTheme { pub highlights: Vec<(String, HighlightStyle)>, - // todo!("Remove this in favor of StatusColor.hint") - // If this should be overridable we should move it to ThemeColors - pub inlay_style: HighlightStyle, - // todo!("Remove this in favor of StatusColor.prediction") - // If this should be overridable we should move it to ThemeColors - pub suggestion_style: HighlightStyle, } impl SyntaxTheme { @@ -72,8 +66,6 @@ impl SyntaxTheme { ("variable.special".into(), red().light().step_9().into()), ("variant".into(), red().light().step_9().into()), ], - inlay_style: tomato().light().step_1().into(), // todo!("nate: use a proper style") - suggestion_style: orange().light().step_1().into(), // todo!("nate: use proper style") } } @@ -132,8 +124,6 @@ impl SyntaxTheme { ("variable.special".into(), red().dark().step_11().into()), ("variant".into(), red().dark().step_11().into()), ], - inlay_style: neutral().dark().step_11().into(), // todo!("nate: use a proper style") - suggestion_style: orange().dark().step_11().into(), // todo!("nate: use a proper style") } } @@ -152,8 +142,6 @@ impl SyntaxTheme { ) }) .collect(), - inlay_style: HighlightStyle::default(), - suggestion_style: HighlightStyle::default(), } } From 9e1d79744554fc9f84dc70db68a6d9453b8f62c8 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 16:17:10 +0200 Subject: [PATCH 71/90] Use distinct version for zed2, append git hash to its nightly version --- .github/workflows/release_nightly.yml | 3 ++- Cargo.lock | 2 +- crates/zed2/Cargo.toml | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release_nightly.yml b/.github/workflows/release_nightly.yml index 38552646c343d5f44975e33146a4cd38dc3ab4d3..0e0fd18e25324984f64405facb2d876f2e68405a 100644 --- a/.github/workflows/release_nightly.yml +++ b/.github/workflows/release_nightly.yml @@ -81,11 +81,12 @@ jobs: - name: Limit target directory size run: script/clear-target-dir-if-larger-than 100 - - name: Set release channel to nightly + - name: Set release channel to nightly, add nightly prefix to the final version run: | set -eu version=$(git rev-parse --short HEAD) echo "Publishing version: ${version} on release channel nightly" + sed -i '' "s/version = \"\(.*\)\"/version = \"\1-nightly\"/" crates/zed2/Cargo.toml echo "nightly" > crates/zed/RELEASE_CHANNEL - name: Generate license file diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19b8fcf1049e7d762db189a50730359a..6935b2810acc40bbd19d36ca8bde66a5c43806c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11889,7 +11889,7 @@ dependencies = [ [[package]] name = "zed2" -version = "0.109.0" +version = "2.0.0" dependencies = [ "activity_indicator2", "ai2", diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 8c0c932f288bbf6a186869257ccdbe99d19bf13a..0fcbcc40fc281b45ad304d48149aec89506c8275 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -2,7 +2,7 @@ description = "The fast, collaborative code editor." edition = "2021" name = "zed2" -version = "0.109.0" +version = "2.0.0" publish = false [lib] From 5644815c4c291ed787729e18860f706b5a39bb8d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 22:06:30 +0200 Subject: [PATCH 72/90] Use a better name for zed2 inlay style field --- crates/editor2/src/display_map.rs | 2 +- crates/editor2/src/editor.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/editor2/src/display_map.rs b/crates/editor2/src/display_map.rs index 9cc16933711c221a3605bbeca01ba13931048a7b..60975a7a5caec6552b3154e528b9629e9beb094c 100644 --- a/crates/editor2/src/display_map.rs +++ b/crates/editor2/src/display_map.rs @@ -513,7 +513,7 @@ impl DisplaySnapshot { self.chunks( display_rows, language_aware, - Some(editor_style.hints_style), + Some(editor_style.inlays_style), Some(editor_style.suggestions_style), ) .map(|chunk| { diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 3dbdbf5e3c400ce4b99c441d60ed44770a0f9164..94ae8abc71a2d99ff2b5bde104809a824cab3877 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -499,7 +499,7 @@ pub struct EditorStyle { pub scrollbar_width: Pixels, pub syntax: Arc, pub diagnostic_style: DiagnosticStyle, - pub hints_style: HighlightStyle, + pub inlays_style: HighlightStyle, pub suggestions_style: HighlightStyle, } @@ -7643,7 +7643,7 @@ impl Editor { .diagnostic_style .clone(), // todo!("what about the rest of the highlight style parts for inlays and suggestions?") - hints_style: HighlightStyle { + inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), font_weight: Some(FontWeight::BOLD), fade_out: Some(0.6), @@ -9318,7 +9318,7 @@ impl Render for Editor { diagnostic_style: cx.theme().diagnostic_style(), // TODO kb find `HighlightStyle` usages // todo!("what about the rest of the highlight style parts?") - hints_style: HighlightStyle { + inlays_style: HighlightStyle { color: Some(cx.theme().status().hint), font_weight: Some(FontWeight::BOLD), fade_out: Some(0.6), From bcdefb8ec84ce3f721c4f7f851b3dca6425a76fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:36:26 -0800 Subject: [PATCH 73/90] Bring back channel notes --- crates/collab_ui2/src/channel_view.rs | 898 +++++++++++++------------- crates/collab_ui2/src/collab_panel.rs | 4 +- 2 files changed, 446 insertions(+), 456 deletions(-) diff --git a/crates/collab_ui2/src/channel_view.rs b/crates/collab_ui2/src/channel_view.rs index d2ffc0de57d865d6c8eb047ff85bad007074e4f3..8d2c037f9d3f5807e7d5375df6accb0b676377d6 100644 --- a/crates/collab_ui2/src/channel_view.rs +++ b/crates/collab_ui2/src/channel_view.rs @@ -1,454 +1,444 @@ -// use anyhow::{anyhow, Result}; -// use call::report_call_event_for_channel; -// use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; -// use client::{ -// proto::{self, PeerId}, -// Collaborator, ParticipantIndex, -// }; -// use collections::HashMap; -// use editor::{CollaborationHub, Editor}; -// use gpui::{ -// actions, -// elements::{ChildView, Label}, -// geometry::vector::Vector2F, -// AnyElement, AnyViewHandle, AppContext, Element, Entity, ModelHandle, Subscription, Task, View, -// ViewContext, ViewHandle, -// }; -// use project::Project; -// use smallvec::SmallVec; -// use std::{ -// any::{Any, TypeId}, -// sync::Arc, -// }; -// use util::ResultExt; -// use workspace::{ -// item::{FollowableItem, Item, ItemEvent, ItemHandle}, -// register_followable_item, -// searchable::SearchableItemHandle, -// ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, -// }; - -// actions!(channel_view, [Deploy]); - -// pub fn init(cx: &mut AppContext) { -// register_followable_item::(cx) -// } - -// pub struct ChannelView { -// pub editor: ViewHandle, -// project: ModelHandle, -// channel_store: ModelHandle, -// channel_buffer: ModelHandle, -// remote_id: Option, -// _editor_event_subscription: Subscription, -// } - -// impl ChannelView { -// pub fn open( -// channel_id: ChannelId, -// workspace: ViewHandle, -// cx: &mut AppContext, -// ) -> Task>> { -// let pane = workspace.read(cx).active_pane().clone(); -// let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); -// cx.spawn(|mut cx| async move { -// let channel_view = channel_view.await?; -// pane.update(&mut cx, |pane, cx| { -// report_call_event_for_channel( -// "open channel notes", -// channel_id, -// &workspace.read(cx).app_state().client, -// cx, -// ); -// pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); -// }); -// anyhow::Ok(channel_view) -// }) -// } - -// pub fn open_in_pane( -// channel_id: ChannelId, -// pane: ViewHandle, -// workspace: ViewHandle, -// cx: &mut AppContext, -// ) -> Task>> { -// let workspace = workspace.read(cx); -// let project = workspace.project().to_owned(); -// let channel_store = ChannelStore::global(cx); -// let language_registry = workspace.app_state().languages.clone(); -// let markdown = language_registry.language_for_name("Markdown"); -// let channel_buffer = -// channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); - -// cx.spawn(|mut cx| async move { -// let channel_buffer = channel_buffer.await?; -// let markdown = markdown.await.log_err(); - -// channel_buffer.update(&mut cx, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.set_language_registry(language_registry); -// if let Some(markdown) = markdown { -// buffer.set_language(Some(markdown), cx); -// } -// }) -// }); - -// pane.update(&mut cx, |pane, cx| { -// let buffer_id = channel_buffer.read(cx).remote_id(cx); - -// let existing_view = pane -// .items_of_type::() -// .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); - -// // If this channel buffer is already open in this pane, just return it. -// if let Some(existing_view) = existing_view.clone() { -// if existing_view.read(cx).channel_buffer == channel_buffer { -// return existing_view; -// } -// } - -// let view = cx.add_view(|cx| { -// let mut this = Self::new(project, channel_store, channel_buffer, cx); -// this.acknowledge_buffer_version(cx); -// this -// }); - -// // If the pane contained a disconnected view for this channel buffer, -// // replace that. -// if let Some(existing_item) = existing_view { -// if let Some(ix) = pane.index_for_item(&existing_item) { -// pane.close_item_by_id(existing_item.id(), SaveIntent::Skip, cx) -// .detach(); -// pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); -// } -// } - -// view -// }) -// .ok_or_else(|| anyhow!("pane was dropped")) -// }) -// } - -// pub fn new( -// project: ModelHandle, -// channel_store: ModelHandle, -// channel_buffer: ModelHandle, -// cx: &mut ViewContext, -// ) -> Self { -// let buffer = channel_buffer.read(cx).buffer(); -// let editor = cx.add_view(|cx| { -// let mut editor = Editor::for_buffer(buffer, None, cx); -// editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( -// channel_buffer.clone(), -// ))); -// editor.set_read_only( -// !channel_buffer -// .read(cx) -// .channel(cx) -// .is_some_and(|c| c.can_edit_notes()), -// ); -// editor -// }); -// let _editor_event_subscription = cx.subscribe(&editor, |_, _, e, cx| cx.emit(e.clone())); - -// cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) -// .detach(); - -// Self { -// editor, -// project, -// channel_store, -// channel_buffer, -// remote_id: None, -// _editor_event_subscription, -// } -// } - -// pub fn channel(&self, cx: &AppContext) -> Option> { -// self.channel_buffer.read(cx).channel(cx) -// } - -// fn handle_channel_buffer_event( -// &mut self, -// _: ModelHandle, -// event: &ChannelBufferEvent, -// cx: &mut ViewContext, -// ) { -// match event { -// ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { -// editor.set_read_only(true); -// cx.notify(); -// }), -// ChannelBufferEvent::ChannelChanged => { -// self.editor.update(cx, |editor, cx| { -// editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); -// cx.emit(editor::Event::TitleChanged); -// cx.notify() -// }); -// } -// ChannelBufferEvent::BufferEdited => { -// if cx.is_self_focused() || self.editor.is_focused(cx) { -// self.acknowledge_buffer_version(cx); -// } else { -// self.channel_store.update(cx, |store, cx| { -// let channel_buffer = self.channel_buffer.read(cx); -// store.notes_changed( -// channel_buffer.channel_id, -// channel_buffer.epoch(), -// &channel_buffer.buffer().read(cx).version(), -// cx, -// ) -// }); -// } -// } -// ChannelBufferEvent::CollaboratorsChanged => {} -// } -// } - -// fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext<'_, '_, ChannelView>) { -// self.channel_store.update(cx, |store, cx| { -// let channel_buffer = self.channel_buffer.read(cx); -// store.acknowledge_notes_version( -// channel_buffer.channel_id, -// channel_buffer.epoch(), -// &channel_buffer.buffer().read(cx).version(), -// cx, -// ) -// }); -// self.channel_buffer.update(cx, |buffer, cx| { -// buffer.acknowledge_buffer_version(cx); -// }); -// } -// } - -// impl Entity for ChannelView { -// type Event = editor::Event; -// } - -// impl View for ChannelView { -// fn ui_name() -> &'static str { -// "ChannelView" -// } - -// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { -// ChildView::new(self.editor.as_any(), cx).into_any() -// } - -// fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { -// if cx.is_self_focused() { -// self.acknowledge_buffer_version(cx); -// cx.focus(self.editor.as_any()) -// } -// } -// } - -// impl Item for ChannelView { -// fn act_as_type<'a>( -// &'a self, -// type_id: TypeId, -// self_handle: &'a ViewHandle, -// _: &'a AppContext, -// ) -> Option<&'a AnyViewHandle> { -// if type_id == TypeId::of::() { -// Some(self_handle) -// } else if type_id == TypeId::of::() { -// Some(&self.editor) -// } else { -// None -// } -// } - -// fn tab_content( -// &self, -// _: Option, -// style: &theme::Tab, -// cx: &gpui::AppContext, -// ) -> AnyElement { -// let label = if let Some(channel) = self.channel(cx) { -// match ( -// channel.can_edit_notes(), -// self.channel_buffer.read(cx).is_connected(), -// ) { -// (true, true) => format!("#{}", channel.name), -// (false, true) => format!("#{} (read-only)", channel.name), -// (_, false) => format!("#{} (disconnected)", channel.name), -// } -// } else { -// format!("channel notes (disconnected)") -// }; -// Label::new(label, style.label.to_owned()).into_any() -// } - -// fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option { -// Some(Self::new( -// self.project.clone(), -// self.channel_store.clone(), -// self.channel_buffer.clone(), -// cx, -// )) -// } - -// fn is_singleton(&self, _cx: &AppContext) -> bool { -// false -// } - -// fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { -// self.editor -// .update(cx, |editor, cx| editor.navigate(data, cx)) -// } - -// fn deactivated(&mut self, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| Item::deactivated(editor, cx)) -// } - -// fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { -// self.editor -// .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) -// } - -// fn as_searchable(&self, _: &ViewHandle) -> Option> { -// Some(Box::new(self.editor.clone())) -// } - -// fn show_toolbar(&self) -> bool { -// true -// } - -// fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option { -// self.editor.read(cx).pixel_position_of_cursor(cx) -// } - -// fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { -// editor::Editor::to_item_events(event) -// } -// } - -// impl FollowableItem for ChannelView { -// fn remote_id(&self) -> Option { -// self.remote_id -// } - -// fn to_state_proto(&self, cx: &AppContext) -> Option { -// let channel_buffer = self.channel_buffer.read(cx); -// if !channel_buffer.is_connected() { -// return None; -// } - -// Some(proto::view::Variant::ChannelView( -// proto::view::ChannelView { -// channel_id: channel_buffer.channel_id, -// editor: if let Some(proto::view::Variant::Editor(proto)) = -// self.editor.read(cx).to_state_proto(cx) -// { -// Some(proto) -// } else { -// None -// }, -// }, -// )) -// } - -// fn from_state_proto( -// pane: ViewHandle, -// workspace: ViewHandle, -// remote_id: workspace::ViewId, -// state: &mut Option, -// cx: &mut AppContext, -// ) -> Option>>> { -// let Some(proto::view::Variant::ChannelView(_)) = state else { -// return None; -// }; -// let Some(proto::view::Variant::ChannelView(state)) = state.take() else { -// unreachable!() -// }; - -// let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); - -// Some(cx.spawn(|mut cx| async move { -// let this = open.await?; - -// let task = this -// .update(&mut cx, |this, cx| { -// this.remote_id = Some(remote_id); - -// if let Some(state) = state.editor { -// Some(this.editor.update(cx, |editor, cx| { -// editor.apply_update_proto( -// &this.project, -// proto::update_view::Variant::Editor(proto::update_view::Editor { -// selections: state.selections, -// pending_selection: state.pending_selection, -// scroll_top_anchor: state.scroll_top_anchor, -// scroll_x: state.scroll_x, -// scroll_y: state.scroll_y, -// ..Default::default() -// }), -// cx, -// ) -// })) -// } else { -// None -// } -// }) -// .ok_or_else(|| anyhow!("window was closed"))?; - -// if let Some(task) = task { -// task.await?; -// } - -// Ok(this) -// })) -// } - -// fn add_event_to_update_proto( -// &self, -// event: &Self::Event, -// update: &mut Option, -// cx: &AppContext, -// ) -> bool { -// self.editor -// .read(cx) -// .add_event_to_update_proto(event, update, cx) -// } - -// fn apply_update_proto( -// &mut self, -// project: &ModelHandle, -// message: proto::update_view::Variant, -// cx: &mut ViewContext, -// ) -> gpui::Task> { -// self.editor.update(cx, |editor, cx| { -// editor.apply_update_proto(project, message, cx) -// }) -// } - -// fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { -// self.editor.update(cx, |editor, cx| { -// editor.set_leader_peer_id(leader_peer_id, cx) -// }) -// } - -// fn should_unfollow_on_event(event: &Self::Event, cx: &AppContext) -> bool { -// Editor::should_unfollow_on_event(event, cx) -// } - -// fn is_project_item(&self, _cx: &AppContext) -> bool { -// false -// } -// } - -// struct ChannelBufferCollaborationHub(ModelHandle); - -// impl CollaborationHub for ChannelBufferCollaborationHub { -// fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { -// self.0.read(cx).collaborators() -// } - -// fn user_participant_indices<'a>( -// &self, -// cx: &'a AppContext, -// ) -> &'a HashMap { -// self.0.read(cx).user_store().read(cx).participant_indices() -// } -// } +use anyhow::Result; +use call::report_call_event_for_channel; +use channel::{Channel, ChannelBuffer, ChannelBufferEvent, ChannelId, ChannelStore}; +use client::{ + proto::{self, PeerId}, + Collaborator, ParticipantIndex, +}; +use collections::HashMap; +use editor::{CollaborationHub, Editor, EditorEvent}; +use gpui::{ + actions, AnyElement, AnyView, AppContext, Entity as _, EventEmitter, FocusableView, + IntoElement as _, Model, Pixels, Point, Render, Subscription, Task, View, ViewContext, + VisualContext as _, WindowContext, +}; +use project::Project; +use std::{ + any::{Any, TypeId}, + sync::Arc, +}; +use ui::Label; +use util::ResultExt; +use workspace::{ + item::{FollowableItem, Item, ItemEvent, ItemHandle}, + register_followable_item, + searchable::SearchableItemHandle, + ItemNavHistory, Pane, SaveIntent, ViewId, Workspace, WorkspaceId, +}; + +actions!(Deploy); + +pub fn init(cx: &mut AppContext) { + register_followable_item::(cx) +} + +pub struct ChannelView { + pub editor: View, + project: Model, + channel_store: Model, + channel_buffer: Model, + remote_id: Option, + _editor_event_subscription: Subscription, +} + +impl ChannelView { + pub fn open( + channel_id: ChannelId, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { + let pane = workspace.read(cx).active_pane().clone(); + let channel_view = Self::open_in_pane(channel_id, pane.clone(), workspace.clone(), cx); + cx.spawn(|mut cx| async move { + let channel_view = channel_view.await?; + pane.update(&mut cx, |pane, cx| { + report_call_event_for_channel( + "open channel notes", + channel_id, + &workspace.read(cx).app_state().client, + cx, + ); + pane.add_item(Box::new(channel_view.clone()), true, true, None, cx); + })?; + anyhow::Ok(channel_view) + }) + } + + pub fn open_in_pane( + channel_id: ChannelId, + pane: View, + workspace: View, + cx: &mut WindowContext, + ) -> Task>> { + let workspace = workspace.read(cx); + let project = workspace.project().to_owned(); + let channel_store = ChannelStore::global(cx); + let language_registry = workspace.app_state().languages.clone(); + let markdown = language_registry.language_for_name("Markdown"); + let channel_buffer = + channel_store.update(cx, |store, cx| store.open_channel_buffer(channel_id, cx)); + + cx.spawn(|mut cx| async move { + let channel_buffer = channel_buffer.await?; + let markdown = markdown.await.log_err(); + + channel_buffer.update(&mut cx, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.set_language_registry(language_registry); + if let Some(markdown) = markdown { + buffer.set_language(Some(markdown), cx); + } + }) + })?; + + pane.update(&mut cx, |pane, cx| { + let buffer_id = channel_buffer.read(cx).remote_id(cx); + + let existing_view = pane + .items_of_type::() + .find(|view| view.read(cx).channel_buffer.read(cx).remote_id(cx) == buffer_id); + + // If this channel buffer is already open in this pane, just return it. + if let Some(existing_view) = existing_view.clone() { + if existing_view.read(cx).channel_buffer == channel_buffer { + return existing_view; + } + } + + let view = cx.build_view(|cx| { + let mut this = Self::new(project, channel_store, channel_buffer, cx); + this.acknowledge_buffer_version(cx); + this + }); + + // If the pane contained a disconnected view for this channel buffer, + // replace that. + if let Some(existing_item) = existing_view { + if let Some(ix) = pane.index_for_item(&existing_item) { + pane.close_item_by_id(existing_item.entity_id(), SaveIntent::Skip, cx) + .detach(); + pane.add_item(Box::new(view.clone()), true, true, Some(ix), cx); + } + } + + view + }) + }) + } + + pub fn new( + project: Model, + channel_store: Model, + channel_buffer: Model, + cx: &mut ViewContext, + ) -> Self { + let buffer = channel_buffer.read(cx).buffer(); + let editor = cx.build_view(|cx| { + let mut editor = Editor::for_buffer(buffer, None, cx); + editor.set_collaboration_hub(Box::new(ChannelBufferCollaborationHub( + channel_buffer.clone(), + ))); + editor.set_read_only( + !channel_buffer + .read(cx) + .channel(cx) + .is_some_and(|c| c.can_edit_notes()), + ); + editor + }); + let _editor_event_subscription = + cx.subscribe(&editor, |_, _, e: &EditorEvent, cx| cx.emit(e.clone())); + + cx.subscribe(&channel_buffer, Self::handle_channel_buffer_event) + .detach(); + + Self { + editor, + project, + channel_store, + channel_buffer, + remote_id: None, + _editor_event_subscription, + } + } + + pub fn channel(&self, cx: &AppContext) -> Option> { + self.channel_buffer.read(cx).channel(cx) + } + + fn handle_channel_buffer_event( + &mut self, + _: Model, + event: &ChannelBufferEvent, + cx: &mut ViewContext, + ) { + match event { + ChannelBufferEvent::Disconnected => self.editor.update(cx, |editor, cx| { + editor.set_read_only(true); + cx.notify(); + }), + ChannelBufferEvent::ChannelChanged => { + self.editor.update(cx, |editor, cx| { + editor.set_read_only(!self.channel(cx).is_some_and(|c| c.can_edit_notes())); + cx.emit(editor::EditorEvent::TitleChanged); + cx.notify() + }); + } + ChannelBufferEvent::BufferEdited => { + if self.editor.read(cx).is_focused(cx) { + self.acknowledge_buffer_version(cx); + } else { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.notes_changed( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + } + } + ChannelBufferEvent::CollaboratorsChanged => {} + } + } + + fn acknowledge_buffer_version(&mut self, cx: &mut ViewContext) { + self.channel_store.update(cx, |store, cx| { + let channel_buffer = self.channel_buffer.read(cx); + store.acknowledge_notes_version( + channel_buffer.channel_id, + channel_buffer.epoch(), + &channel_buffer.buffer().read(cx).version(), + cx, + ) + }); + self.channel_buffer.update(cx, |buffer, cx| { + buffer.acknowledge_buffer_version(cx); + }); + } +} + +impl EventEmitter for ChannelView {} + +impl Render for ChannelView { + type Element = AnyView; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.editor.clone().into() + } +} + +impl FocusableView for ChannelView { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.editor.read(cx).focus_handle(cx) + } +} + +impl Item for ChannelView { + type Event = EditorEvent; + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn tab_content(&self, _: Option, cx: &WindowContext) -> AnyElement { + let label = if let Some(channel) = self.channel(cx) { + match ( + channel.can_edit_notes(), + self.channel_buffer.read(cx).is_connected(), + ) { + (true, true) => format!("#{}", channel.name), + (false, true) => format!("#{} (read-only)", channel.name), + (_, false) => format!("#{} (disconnected)", channel.name), + } + } else { + format!("channel notes (disconnected)") + }; + Label::new(label).into_any_element() + } + + fn clone_on_split(&self, _: WorkspaceId, cx: &mut ViewContext) -> Option> { + Some(cx.build_view(|cx| { + Self::new( + self.project.clone(), + self.channel_store.clone(), + self.channel_buffer.clone(), + cx, + ) + })) + } + + fn is_singleton(&self, _cx: &AppContext) -> bool { + false + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::deactivated(editor, cx)) + } + + fn set_nav_history(&mut self, history: ItemNavHistory, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| Item::set_nav_history(editor, history, cx)) + } + + fn as_searchable(&self, _: &View) -> Option> { + Some(Box::new(self.editor.clone())) + } + + fn show_toolbar(&self) -> bool { + true + } + + fn pixel_position_of_cursor(&self, cx: &AppContext) -> Option> { + self.editor.read(cx).pixel_position_of_cursor(cx) + } + + fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { + Editor::to_item_events(event, f) + } +} + +impl FollowableItem for ChannelView { + fn remote_id(&self) -> Option { + self.remote_id + } + + fn to_state_proto(&self, cx: &WindowContext) -> Option { + let channel_buffer = self.channel_buffer.read(cx); + if !channel_buffer.is_connected() { + return None; + } + + Some(proto::view::Variant::ChannelView( + proto::view::ChannelView { + channel_id: channel_buffer.channel_id, + editor: if let Some(proto::view::Variant::Editor(proto)) = + self.editor.read(cx).to_state_proto(cx) + { + Some(proto) + } else { + None + }, + }, + )) + } + + fn from_state_proto( + pane: View, + workspace: View, + remote_id: workspace::ViewId, + state: &mut Option, + cx: &mut WindowContext, + ) -> Option>>> { + let Some(proto::view::Variant::ChannelView(_)) = state else { + return None; + }; + let Some(proto::view::Variant::ChannelView(state)) = state.take() else { + unreachable!() + }; + + let open = ChannelView::open_in_pane(state.channel_id, pane, workspace, cx); + + Some(cx.spawn(|mut cx| async move { + let this = open.await?; + + let task = this.update(&mut cx, |this, cx| { + this.remote_id = Some(remote_id); + + if let Some(state) = state.editor { + Some(this.editor.update(cx, |editor, cx| { + editor.apply_update_proto( + &this.project, + proto::update_view::Variant::Editor(proto::update_view::Editor { + selections: state.selections, + pending_selection: state.pending_selection, + scroll_top_anchor: state.scroll_top_anchor, + scroll_x: state.scroll_x, + scroll_y: state.scroll_y, + ..Default::default() + }), + cx, + ) + })) + } else { + None + } + })?; + + if let Some(task) = task { + task.await?; + } + + Ok(this) + })) + } + + fn add_event_to_update_proto( + &self, + event: &EditorEvent, + update: &mut Option, + cx: &WindowContext, + ) -> bool { + self.editor + .read(cx) + .add_event_to_update_proto(event, update, cx) + } + + fn apply_update_proto( + &mut self, + project: &Model, + message: proto::update_view::Variant, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |editor, cx| { + editor.apply_update_proto(project, message, cx) + }) + } + + fn set_leader_peer_id(&mut self, leader_peer_id: Option, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| { + editor.set_leader_peer_id(leader_peer_id, cx) + }) + } + + fn is_project_item(&self, _cx: &WindowContext) -> bool { + false + } + + fn to_follow_event(event: &Self::Event) -> Option { + Editor::to_follow_event(event) + } +} + +struct ChannelBufferCollaborationHub(Model); + +impl CollaborationHub for ChannelBufferCollaborationHub { + fn collaborators<'a>(&self, cx: &'a AppContext) -> &'a HashMap { + self.0.read(cx).collaborators() + } + + fn user_participant_indices<'a>( + &self, + cx: &'a AppContext, + ) -> &'a HashMap { + self.0.read(cx).user_store().read(cx).participant_indices() + } +} diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 8c0a92ad5252c5b12bc56ee174329a15a5a384c2..272f02bb49105bc55c2b4ca8a0755a4e045e0342 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -191,6 +191,7 @@ use workspace::{ Workspace, }; +use crate::channel_view::ChannelView; use crate::{face_pile::FacePile, CollaborationPanelSettings}; use self::channel_modal::ChannelModal; @@ -1935,8 +1936,7 @@ impl CollabPanel { fn open_channel_notes(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { if let Some(workspace) = self.workspace.upgrade() { - todo!(); - // ChannelView::open(action.channel_id, workspace, cx).detach(); + ChannelView::open(channel_id, workspace, cx).detach(); } } From 1ec81e02da8b7d24648186ad3c58d02583ed5ffb Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:44:07 -0800 Subject: [PATCH 74/90] Allow opening channel notes from the channel list --- crates/collab_ui2/src/collab_panel.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 272f02bb49105bc55c2b4ca8a0755a4e045e0342..bfef193cf73cc3b4129815bf305d88653d7e88ba 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2619,6 +2619,9 @@ impl CollabPanel { } else { Color::Muted }) + .on_click(cx.listener(move |this, _, cx| { + this.open_channel_notes(channel_id, cx) + })) .tooltip(|cx| { Tooltip::text("Open channel notes", cx) }), From e9dcca7712569f7573610c62a0d5376c09c54d6e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 5 Dec 2023 15:59:54 -0800 Subject: [PATCH 75/90] Initialize channel notes to register the view as followable --- crates/collab_ui2/src/collab_ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index efd3ff869225aced36002a3bdb4f1f5905579c5a..fba76932fc01749eaa8ddee7c19264aaecae455e 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -33,6 +33,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { // vcs_menu::init(cx); collab_titlebar_item::init(cx); collab_panel::init(cx); + channel_view::init(cx); // chat_panel::init(cx); notifications::init(&app_state, cx); From 6bbb1642b86545da18119d1e729145cfbc2e33b1 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 12:18:48 -0800 Subject: [PATCH 76/90] Fix propagation of active item to followers Enable channel buffer integration tests. --- Cargo.lock | 2 +- crates/collab2/Cargo.toml | 2 +- .../collab2/src/tests/channel_buffer_tests.rs | 1756 +++++++++-------- crates/collab2/src/tests/test_server.rs | 14 +- crates/gpui2/src/app/test_context.rs | 4 + crates/workspace2/src/workspace2.rs | 25 +- 6 files changed, 906 insertions(+), 897 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19b8fcf1049e7d762db189a50730359a..773c2a3df552928594755ca398e38ac6861f6a66 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1830,7 +1830,7 @@ dependencies = [ "clap 3.2.25", "client2", "clock", - "collab_ui", + "collab_ui2", "collections", "ctor", "dashmap", diff --git a/crates/collab2/Cargo.toml b/crates/collab2/Cargo.toml index b8e6a45b06959e44958d7cd3d9e77d7fa8bb49e2..06443afdbcf05190d67efcdccc8cba8cb03bbcdd 100644 --- a/crates/collab2/Cargo.toml +++ b/crates/collab2/Cargo.toml @@ -81,7 +81,7 @@ settings = { package = "settings2", path = "../settings2", features = ["test-sup theme = { package = "theme2", path = "../theme2" } workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } -collab_ui = { path = "../collab_ui", features = ["test-support"] } +collab_ui = { path = "../collab_ui2", package = "collab_ui2", features = ["test-support"] } async-trait.workspace = true pretty_assertions.workspace = true diff --git a/crates/collab2/src/tests/channel_buffer_tests.rs b/crates/collab2/src/tests/channel_buffer_tests.rs index 63057cbd415f011f9269c0f61410afbd12fb0721..b0af360fd52ee78300060e669ba38620d07ec4e7 100644 --- a/crates/collab2/src/tests/channel_buffer_tests.rs +++ b/crates/collab2/src/tests/channel_buffer_tests.rs @@ -1,875 +1,881 @@ -//todo(partially ported) -// use std::ops::Range; - -// use crate::{ -// rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, -// tests::TestServer, -// }; -// use client::{Collaborator, ParticipantIndex, UserId}; -// use collections::HashMap; -// use editor::{Anchor, Editor, ToOffset}; -// use futures::future; -// use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; -// use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; - -// #[gpui::test] -// async fn test_core_channel_buffers( -// executor: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(executor.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) -// .await; - -// // Client A joins the channel buffer -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// // Client A edits the buffer -// let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(0..0, "hello world")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(5..5, ", cruel")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| { -// buffer.edit([(0..5, "goodbye")], None, cx) -// }); -// buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); -// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); -// executor.run_until_parked(); - -// // Client B joins the channel buffer -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators( -// buffer.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// }); - -// // Client B sees the correct text, and then edits it -// let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); -// assert_eq!( -// buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), -// buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) -// ); -// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); -// buffer_b.update(cx_b, |buffer, cx| { -// buffer.edit([(7..12, "beautiful")], None, cx) -// }); - -// // Both A and B see the new edit -// executor.run_until_parked(); -// assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); -// assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); - -// // Client A closes the channel buffer. -// cx_a.update(|_| drop(channel_buffer_a)); -// executor.run_until_parked(); - -// // Client B sees that client A is gone from the channel buffer. -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); -// }); - -// // Client A rejoins the channel buffer -// let _channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// executor.run_until_parked(); - -// // Sanity test, make sure we saw A rejoining -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators( -// &buffer.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// }); - -// // Client A loses connection. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// // Client B observes A disconnect -// channel_buffer_b.read_with(cx_b, |buffer, _| { -// assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); -// }); - -// // TODO: -// // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects -// // - Test interaction with channel deletion while buffer is open -// } - -// // todo!("collab_ui") -// // #[gpui::test] -// // async fn test_channel_notes_participant_indices( -// // executor: BackgroundExecutor, -// // mut cx_a: &mut TestAppContext, -// // mut cx_b: &mut TestAppContext, -// // cx_c: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&executor).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; -// // let client_c = server.create_client(cx_c, "user_c").await; - -// // let active_call_a = cx_a.read(ActiveCall::global); -// // let active_call_b = cx_b.read(ActiveCall::global); - -// // cx_a.update(editor::init); -// // cx_b.update(editor::init); -// // cx_c.update(editor::init); - -// // let channel_id = server -// // .make_channel( -// // "the-channel", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; - -// // client_a -// // .fs() -// // .insert_tree("/root", json!({"file.txt": "123"})) -// // .await; -// // let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; -// // let project_b = client_b.build_empty_local_project(cx_b); -// // let project_c = client_c.build_empty_local_project(cx_c); -// // 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); -// // let workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // // Clients A, B, and C open the channel notes -// // let channel_view_a = cx_a -// // .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // let channel_view_b = cx_b -// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// // .await -// // .unwrap(); -// // let channel_view_c = cx_c -// // .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) -// // .await -// // .unwrap(); - -// // // Clients A, B, and C all insert and select some text -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.insert("a", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![0..1]); -// // }); -// // }); -// // }); -// // executor.run_until_parked(); -// // channel_view_b.update(cx_b, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.move_down(&Default::default(), cx); -// // editor.insert("b", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![1..2]); -// // }); -// // }); -// // }); -// // executor.run_until_parked(); -// // channel_view_c.update(cx_c, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // editor.move_down(&Default::default(), cx); -// // editor.insert("c", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![2..3]); -// // }); -// // }); -// // }); - -// // // Client A sees clients B and C without assigned colors, because they aren't -// // // in a call together. -// // executor.run_until_parked(); -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); -// // }); -// // }); - -// // // Clients A and B join the same call. -// // for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { -// // call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) -// // .await -// // .unwrap(); -// // } - -// // // Clients A and B see each other with two different assigned colors. Client C -// // // still doesn't have a color. -// // executor.run_until_parked(); -// // channel_view_a.update(cx_a, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections( -// // editor, -// // &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], -// // cx, -// // ); -// // }); -// // }); -// // channel_view_b.update(cx_b, |notes, cx| { -// // notes.editor.update(cx, |editor, cx| { -// // assert_remote_selections( -// // editor, -// // &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], -// // cx, -// // ); -// // }); -// // }); - -// // // Client A shares a project, and client B joins. -// // 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).root(cx_b); - -// // // Clients A and B open the same file. -// // let editor_a = workspace_a -// // .update(cx_a, |workspace, cx| { -// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// // }) -// // .await -// // .unwrap() -// // .downcast::() -// // .unwrap(); -// // let editor_b = workspace_b -// // .update(cx_b, |workspace, cx| { -// // workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) -// // }) -// // .await -// // .unwrap() -// // .downcast::() -// // .unwrap(); - -// // editor_a.update(cx_a, |editor, cx| { -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![0..1]); -// // }); -// // }); -// // editor_b.update(cx_b, |editor, cx| { -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![2..3]); -// // }); -// // }); -// // executor.run_until_parked(); - -// // // Clients A and B see each other with the same colors as in the channel notes. -// // editor_a.update(cx_a, |editor, cx| { -// // assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); -// // }); -// // editor_b.update(cx_b, |editor, cx| { -// // assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); -// // }); -// // } - -// #[track_caller] -// fn assert_remote_selections( -// editor: &mut Editor, -// expected_selections: &[(Option, Range)], -// cx: &mut ViewContext, -// ) { -// let snapshot = editor.snapshot(cx); -// let range = Anchor::min()..Anchor::max(); -// let remote_selections = snapshot -// .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) -// .map(|s| { -// let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); -// let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); -// (s.participant_index, start..end) -// }) -// .collect::>(); -// assert_eq!( -// remote_selections, expected_selections, -// "incorrect remote selections" -// ); -// } - -// #[gpui::test] -// async fn test_multiple_handles_to_channel_buffer( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; - -// let channel_id = server -// .make_channel("the-channel", None, (&client_a, cx_a), &mut []) -// .await; - -// let channel_buffer_1 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_buffer_2 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); -// let channel_buffer_3 = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); - -// // All concurrent tasks for opening a channel buffer return the same model handle. -// let (channel_buffer, channel_buffer_2, channel_buffer_3) = -// future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) -// .await -// .unwrap(); -// let channel_buffer_model_id = channel_buffer.entity_id(); -// assert_eq!(channel_buffer, channel_buffer_2); -// assert_eq!(channel_buffer, channel_buffer_3); - -// channel_buffer.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "hello")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// cx_a.update(|_| { -// drop(channel_buffer); -// drop(channel_buffer_2); -// drop(channel_buffer_3); -// }); -// deterministic.run_until_parked(); - -// // The channel buffer can be reopened after dropping it. -// let channel_buffer = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); -// channel_buffer.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, _| { -// assert_eq!(buffer.text(), "hello"); -// }) -// }); -// } - -// #[gpui::test] -// async fn test_channel_buffer_disconnect( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); -// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); -// assert!(!buffer.is_connected()); -// }); - -// deterministic.run_until_parked(); - -// server.allow_connections(); -// deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); - -// deterministic.run_until_parked(); - -// client_a -// .channel_store() -// .update(cx_a, |channel_store, _| { -// channel_store.remove_channel(channel_id) -// }) -// .await -// .unwrap(); -// deterministic.run_until_parked(); - -// // Channel buffer observed the deletion -// channel_buffer_b.update(cx_b, |buffer, cx| { -// assert!(buffer.channel(cx).is_none()); -// assert!(!buffer.is_connected()); -// }); -// } - -// #[gpui::test] -// async fn test_rejoin_channel_buffer( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "1")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// // Client A disconnects. -// server.forbid_connections(); -// server.disconnect_client(client_a.peer_id().unwrap()); - -// // Both clients make an edit. -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(1..1, "2")], None, cx); -// }) -// }); -// channel_buffer_b.update(cx_b, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "0")], None, cx); -// }) -// }); - -// // Both clients see their own edit. -// deterministic.run_until_parked(); -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "12"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "01"); -// }); - -// // Client A reconnects. Both clients see each other's edits, and see -// // the same collaborators. -// server.allow_connections(); -// deterministic.advance_clock(RECEIVE_TIMEOUT); -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); - -// channel_buffer_a.read_with(cx_a, |buffer_a, _| { -// channel_buffer_b.read_with(cx_b, |buffer_b, _| { -// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); -// }); -// }); -// } - -// #[gpui::test] -// async fn test_channel_buffers_and_server_restarts( -// deterministic: BackgroundExecutor, -// cx_a: &mut TestAppContext, -// cx_b: &mut TestAppContext, -// cx_c: &mut TestAppContext, -// ) { -// let mut server = TestServer::start(deterministic.clone()).await; -// let client_a = server.create_client(cx_a, "user_a").await; -// let client_b = server.create_client(cx_b, "user_b").await; -// let client_c = server.create_client(cx_c, "user_c").await; - -// let channel_id = server -// .make_channel( -// "the-channel", -// None, -// (&client_a, cx_a), -// &mut [(&client_b, cx_b), (&client_c, cx_c)], -// ) -// .await; - -// let channel_buffer_a = client_a -// .channel_store() -// .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let channel_buffer_b = client_b -// .channel_store() -// .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); -// let _channel_buffer_c = client_c -// .channel_store() -// .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) -// .await -// .unwrap(); - -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "1")], None, cx); -// }) -// }); -// deterministic.run_until_parked(); - -// // Client C can't reconnect. -// client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); - -// // Server stops. -// server.reset().await; -// deterministic.advance_clock(RECEIVE_TIMEOUT); - -// // While the server is down, both clients make an edit. -// channel_buffer_a.update(cx_a, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(1..1, "2")], None, cx); -// }) -// }); -// channel_buffer_b.update(cx_b, |buffer, cx| { -// buffer.buffer().update(cx, |buffer, cx| { -// buffer.edit([(0..0, "0")], None, cx); -// }) -// }); - -// // Server restarts. -// server.start().await.unwrap(); -// deterministic.advance_clock(CLEANUP_TIMEOUT); - -// // Clients reconnects. Clients A and B see each other's edits, and see -// // that client C has disconnected. -// channel_buffer_a.read_with(cx_a, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); -// channel_buffer_b.read_with(cx_b, |buffer, cx| { -// assert_eq!(buffer.buffer().read(cx).text(), "012"); -// }); - -// channel_buffer_a.read_with(cx_a, |buffer_a, _| { -// channel_buffer_b.read_with(cx_b, |buffer_b, _| { -// assert_collaborators( -// buffer_a.collaborators(), -// &[client_a.user_id(), client_b.user_id()], -// ); -// assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); -// }); -// }); -// } - -// //todo!(collab_ui) -// // #[gpui::test(iterations = 10)] -// // async fn test_following_to_channel_notes_without_a_shared_project( -// // deterministic: BackgroundExecutor, -// // mut cx_a: &mut TestAppContext, -// // mut cx_b: &mut TestAppContext, -// // mut cx_c: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; - -// // let client_c = server.create_client(cx_c, "user_c").await; - -// // cx_a.update(editor::init); -// // cx_b.update(editor::init); -// // cx_c.update(editor::init); -// // cx_a.update(collab_ui::channel_view::init); -// // cx_b.update(collab_ui::channel_view::init); -// // cx_c.update(collab_ui::channel_view::init); - -// // let channel_1_id = server -// // .make_channel( -// // "channel-1", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; -// // let channel_2_id = server -// // .make_channel( -// // "channel-2", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b), (&client_c, cx_c)], -// // ) -// // .await; - -// // // Clients A, B, and C join a channel. -// // let active_call_a = cx_a.read(ActiveCall::global); -// // let active_call_b = cx_b.read(ActiveCall::global); -// // let active_call_c = cx_c.read(ActiveCall::global); -// // for (call, cx) in [ -// // (&active_call_a, &mut cx_a), -// // (&active_call_b, &mut cx_b), -// // (&active_call_c, &mut cx_c), -// // ] { -// // call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) -// // .await -// // .unwrap(); -// // } -// // deterministic.run_until_parked(); - -// // // Clients A, B, and C all open their own unshared projects. -// // client_a.fs().insert_tree("/a", json!({})).await; -// // client_b.fs().insert_tree("/b", json!({})).await; -// // client_c.fs().insert_tree("/c", json!({})).await; -// // let (project_a, _) = client_a.build_local_project("/a", cx_a).await; -// // let (project_b, _) = client_b.build_local_project("/b", cx_b).await; -// // let (project_c, _) = client_b.build_local_project("/c", cx_c).await; -// // 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); -// // let _workspace_c = client_c.build_workspace(&project_c, cx_c).root(cx_c); - -// // active_call_a -// // .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) -// // .await -// // .unwrap(); - -// // // Client A opens the notes for channel 1. -// // let channel_view_1_a = cx_a -// // .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // channel_view_1_a.update(cx_a, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// // notes.editor.update(cx, |editor, cx| { -// // editor.insert("Hello from A.", cx); -// // editor.change_selections(None, cx, |selections| { -// // selections.select_ranges(vec![3..4]); -// // }); -// // }); -// // }); - -// // // Client B follows client A. -// // workspace_b -// // .update(cx_b, |workspace, cx| { -// // workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() -// // }) -// // .await -// // .unwrap(); - -// // // Client B is taken to the notes for channel 1, with the same -// // // text selected as client A. -// // deterministic.run_until_parked(); -// // let channel_view_1_b = workspace_b.read_with(cx_b, |workspace, cx| { -// // assert_eq!( -// // workspace.leader_for_pane(workspace.active_pane()), -// // Some(client_a.peer_id().unwrap()) -// // ); -// // workspace -// // .active_item(cx) -// // .expect("no active item") -// // .downcast::() -// // .expect("active item is not a channel view") -// // }); -// // channel_view_1_b.read_with(cx_b, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); -// // let editor = notes.editor.read(cx); -// // assert_eq!(editor.text(cx), "Hello from A."); -// // assert_eq!(editor.selections.ranges::(cx), &[3..4]); -// // }); - -// // // Client A opens the notes for channel 2. -// // let channel_view_2_a = cx_a -// // .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) -// // .await -// // .unwrap(); -// // channel_view_2_a.read_with(cx_a, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// // }); - -// // // Client B is taken to the notes for channel 2. -// // deterministic.run_until_parked(); -// // let channel_view_2_b = workspace_b.read_with(cx_b, |workspace, cx| { -// // assert_eq!( -// // workspace.leader_for_pane(workspace.active_pane()), -// // Some(client_a.peer_id().unwrap()) -// // ); -// // workspace -// // .active_item(cx) -// // .expect("no active item") -// // .downcast::() -// // .expect("active item is not a channel view") -// // }); -// // channel_view_2_b.read_with(cx_b, |notes, cx| { -// // assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); -// // }); -// // } - -// //todo!(collab_ui) -// // #[gpui::test] -// // async fn test_channel_buffer_changes( -// // deterministic: BackgroundExecutor, -// // cx_a: &mut TestAppContext, -// // cx_b: &mut TestAppContext, -// // ) { -// // let mut server = TestServer::start(&deterministic).await; -// // let client_a = server.create_client(cx_a, "user_a").await; -// // let client_b = server.create_client(cx_b, "user_b").await; - -// // let channel_id = server -// // .make_channel( -// // "the-channel", -// // None, -// // (&client_a, cx_a), -// // &mut [(&client_b, cx_b)], -// // ) -// // .await; - -// // let channel_buffer_a = client_a -// // .channel_store() -// // .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) -// // .await -// // .unwrap(); - -// // // Client A makes an edit, and client B should see that the note has changed. -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "1")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.update(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(has_buffer_changed); - -// // // Opening the buffer should clear the changed flag. -// // let project_b = client_b.build_empty_local_project(cx_b); -// // let workspace_b = client_b.build_workspace(&project_b, cx_b).root(cx_b); -// // let channel_view_b = cx_b -// // .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) -// // .await -// // .unwrap(); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.update(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // // Editing the channel while the buffer is open should not show that the buffer has changed. -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "2")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); - -// // // Test that the server is tracking things correctly, and we retain our 'not changed' -// // // state across a disconnect -// // server.simulate_long_connection_interruption(client_b.peer_id().unwrap(), &deterministic); -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(!has_buffer_changed); - -// // // Closing the buffer should re-enable change tracking -// // cx_b.update(|cx| { -// // workspace_b.update(cx, |workspace, cx| { -// // workspace.close_all_items_and_panes(&Default::default(), cx) -// // }); - -// // drop(channel_view_b) -// // }); - -// // deterministic.run_until_parked(); - -// // channel_buffer_a.update(cx_a, |buffer, cx| { -// // buffer.buffer().update(cx, |buffer, cx| { -// // buffer.edit([(0..0, "3")], None, cx); -// // }) -// // }); -// // deterministic.run_until_parked(); - -// // let has_buffer_changed = cx_b.read(|cx| { -// // client_b -// // .channel_store() -// // .read(cx) -// // .has_channel_buffer_changed(channel_id) -// // .unwrap() -// // }); -// // assert!(has_buffer_changed); -// // } - -// #[track_caller] -// fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { -// let mut user_ids = collaborators -// .values() -// .map(|collaborator| collaborator.user_id) -// .collect::>(); -// user_ids.sort(); -// assert_eq!( -// user_ids, -// ids.into_iter().map(|id| id.unwrap()).collect::>() -// ); -// } - -// fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { -// channel_buffer.read_with(cx, |buffer, _| buffer.text()) -// } +use crate::{ + rpc::{CLEANUP_TIMEOUT, RECONNECT_TIMEOUT}, + tests::TestServer, +}; +use call::ActiveCall; +use channel::ACKNOWLEDGE_DEBOUNCE_INTERVAL; +use client::{Collaborator, ParticipantIndex, UserId}; +use collab_ui::channel_view::ChannelView; +use collections::HashMap; +use editor::{Anchor, Editor, ToOffset}; +use futures::future; +use gpui::{BackgroundExecutor, Model, TestAppContext, ViewContext}; +use rpc::{proto::PeerId, RECEIVE_TIMEOUT}; +use serde_json::json; +use std::ops::Range; + +#[gpui::test] +async fn test_core_channel_buffers( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel("zed", None, (&client_a, cx_a), &mut [(&client_b, cx_b)]) + .await; + + // Client A joins the channel buffer + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A edits the buffer + let buffer_a = channel_buffer_a.read_with(cx_a, |buffer, _| buffer.buffer()); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..0, "hello world")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(5..5, ", cruel")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| { + buffer.edit([(0..5, "goodbye")], None, cx) + }); + buffer_a.update(cx_a, |buffer, cx| buffer.undo(cx)); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, cruel world"); + executor.run_until_parked(); + + // Client B joins the channel buffer + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client B sees the correct text, and then edits it + let buffer_b = channel_buffer_b.read_with(cx_b, |buffer, _| buffer.buffer()); + assert_eq!( + buffer_b.read_with(cx_b, |buffer, _| buffer.remote_id()), + buffer_a.read_with(cx_a, |buffer, _| buffer.remote_id()) + ); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, cruel world"); + buffer_b.update(cx_b, |buffer, cx| { + buffer.edit([(7..12, "beautiful")], None, cx) + }); + + // Both A and B see the new edit + executor.run_until_parked(); + assert_eq!(buffer_text(&buffer_a, cx_a), "hello, beautiful world"); + assert_eq!(buffer_text(&buffer_b, cx_b), "hello, beautiful world"); + + // Client A closes the channel buffer. + cx_a.update(|_| drop(channel_buffer_a)); + executor.run_until_parked(); + + // Client B sees that client A is gone from the channel buffer. + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // Client A rejoins the channel buffer + let _channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + executor.run_until_parked(); + + // Sanity test, make sure we saw A rejoining + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators( + &buffer.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + }); + + // Client A loses connection. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + executor.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + // Client B observes A disconnect + channel_buffer_b.read_with(cx_b, |buffer, _| { + assert_collaborators(&buffer.collaborators(), &[client_b.user_id()]); + }); + + // TODO: + // - Test synchronizing offline updates, what happens to A's channel buffer when A disconnects + // - Test interaction with channel deletion while buffer is open +} + +#[gpui::test] +async fn test_channel_notes_participant_indices( + executor: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(executor.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + client_a + .fs() + .insert_tree("/root", json!({"file.txt": "123"})) + .await; + let (project_a, worktree_id_a) = client_a.build_local_project("/root", cx_a).await; + let project_b = client_b.build_empty_local_project(cx_b); + let project_c = client_c.build_empty_local_project(cx_c); + + let (workspace_a, mut cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, mut cx_b) = client_b.build_workspace(&project_b, cx_b); + let (workspace_c, cx_c) = client_c.build_workspace(&project_c, cx_c); + + // Clients A, B, and C open the channel notes + let channel_view_a = cx_a + .update(|cx| ChannelView::open(channel_id, workspace_a.clone(), cx)) + .await + .unwrap(); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + let channel_view_c = cx_c + .update(|cx| ChannelView::open(channel_id, workspace_c.clone(), cx)) + .await + .unwrap(); + + // Clients A, B, and C all insert and select some text + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.insert("a", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + }); + executor.run_until_parked(); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("b", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![1..2]); + }); + }); + }); + executor.run_until_parked(); + channel_view_c.update(cx_c, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + editor.move_down(&Default::default(), cx); + editor.insert("c", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); + }); + + // Client A sees clients B and C without assigned colors, because they aren't + // in a call together. + executor.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections(editor, &[(None, 1..2), (None, 2..3)], cx); + }); + }); + + // Clients A and B join the same call. + for (call, cx) in [(&active_call_a, &mut cx_a), (&active_call_b, &mut cx_b)] { + call.update(*cx, |call, cx| call.join_channel(channel_id, cx)) + .await + .unwrap(); + } + + // Clients A and B see each other with two different assigned colors. Client C + // still doesn't have a color. + executor.run_until_parked(); + channel_view_a.update(cx_a, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(1)), 1..2), (None, 2..3)], + cx, + ); + }); + }); + channel_view_b.update(cx_b, |notes, cx| { + notes.editor.update(cx, |editor, cx| { + assert_remote_selections( + editor, + &[(Some(ParticipantIndex(0)), 0..1), (None, 2..3)], + cx, + ); + }); + }); + + // Client A shares a project, and client B joins. + 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, cx_b) = client_b.build_workspace(&project_b, cx_b); + + // Clients A and B open the same file. + let editor_a = workspace_a + .update(cx_a, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + let editor_b = workspace_b + .update(cx_b, |workspace, cx| { + workspace.open_path((worktree_id_a, "file.txt"), None, true, cx) + }) + .await + .unwrap() + .downcast::() + .unwrap(); + + editor_a.update(cx_a, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![0..1]); + }); + }); + editor_b.update(cx_b, |editor, cx| { + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![2..3]); + }); + }); + executor.run_until_parked(); + + // Clients A and B see each other with the same colors as in the channel notes. + editor_a.update(cx_a, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(1)), 2..3)], cx); + }); + editor_b.update(cx_b, |editor, cx| { + assert_remote_selections(editor, &[(Some(ParticipantIndex(0)), 0..1)], cx); + }); +} + +#[track_caller] +fn assert_remote_selections( + editor: &mut Editor, + expected_selections: &[(Option, Range)], + cx: &mut ViewContext, +) { + let snapshot = editor.snapshot(cx); + let range = Anchor::min()..Anchor::max(); + let remote_selections = snapshot + .remote_selections_in_range(&range, editor.collaboration_hub().unwrap(), cx) + .map(|s| { + let start = s.selection.start.to_offset(&snapshot.buffer_snapshot); + let end = s.selection.end.to_offset(&snapshot.buffer_snapshot); + (s.participant_index, start..end) + }) + .collect::>(); + assert_eq!( + remote_selections, expected_selections, + "incorrect remote selections" + ); +} + +#[gpui::test] +async fn test_multiple_handles_to_channel_buffer( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + + let channel_id = server + .make_channel("the-channel", None, (&client_a, cx_a), &mut []) + .await; + + let channel_buffer_1 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + let channel_buffer_2 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + let channel_buffer_3 = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)); + + // All concurrent tasks for opening a channel buffer return the same model handle. + let (channel_buffer, channel_buffer_2, channel_buffer_3) = + future::try_join3(channel_buffer_1, channel_buffer_2, channel_buffer_3) + .await + .unwrap(); + let channel_buffer_model_id = channel_buffer.entity_id(); + assert_eq!(channel_buffer, channel_buffer_2); + assert_eq!(channel_buffer, channel_buffer_3); + + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "hello")], None, cx); + }) + }); + deterministic.run_until_parked(); + + cx_a.update(|_| { + drop(channel_buffer); + drop(channel_buffer_2); + drop(channel_buffer_3); + }); + deterministic.run_until_parked(); + + // The channel buffer can be reopened after dropping it. + let channel_buffer = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + assert_ne!(channel_buffer.entity_id(), channel_buffer_model_id); + channel_buffer.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, _| { + assert_eq!(buffer.text(), "hello"); + }) + }); +} + +#[gpui::test] +async fn test_channel_buffer_disconnect( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + channel_buffer_a.update(cx_a, |buffer, cx| { + assert_eq!(buffer.channel(cx).unwrap().name, "the-channel"); + assert!(!buffer.is_connected()); + }); + + deterministic.run_until_parked(); + + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT + RECONNECT_TIMEOUT); + + deterministic.run_until_parked(); + + client_a + .channel_store() + .update(cx_a, |channel_store, _| { + channel_store.remove_channel(channel_id) + }) + .await + .unwrap(); + deterministic.run_until_parked(); + + // Channel buffer observed the deletion + channel_buffer_b.update(cx_b, |buffer, cx| { + assert!(buffer.channel(cx).is_none()); + assert!(!buffer.is_connected()); + }); +} + +#[gpui::test] +async fn test_rejoin_channel_buffer( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client A disconnects. + server.forbid_connections(); + server.disconnect_client(client_a.peer_id().unwrap()); + + // Both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Both clients see their own edit. + deterministic.run_until_parked(); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "12"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "01"); + }); + + // Client A reconnects. Both clients see each other's edits, and see + // the same collaborators. + server.allow_connections(); + deterministic.advance_clock(RECEIVE_TIMEOUT); + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + +#[gpui::test] +async fn test_channel_buffers_and_server_restarts( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, + cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + let client_c = server.create_client(cx_c, "user_c").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let channel_buffer_b = client_b + .channel_store() + .update(cx_b, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + let _channel_buffer_c = client_c + .channel_store() + .update(cx_c, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + // Client C can't reconnect. + client_c.override_establish_connection(|_, cx| cx.spawn(|_| future::pending())); + + // Server stops. + server.reset().await; + deterministic.advance_clock(RECEIVE_TIMEOUT); + + // While the server is down, both clients make an edit. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(1..1, "2")], None, cx); + }) + }); + channel_buffer_b.update(cx_b, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "0")], None, cx); + }) + }); + + // Server restarts. + server.start().await.unwrap(); + deterministic.advance_clock(CLEANUP_TIMEOUT); + + // Clients reconnects. Clients A and B see each other's edits, and see + // that client C has disconnected. + channel_buffer_a.read_with(cx_a, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + channel_buffer_b.read_with(cx_b, |buffer, cx| { + assert_eq!(buffer.buffer().read(cx).text(), "012"); + }); + + channel_buffer_a.read_with(cx_a, |buffer_a, _| { + channel_buffer_b.read_with(cx_b, |buffer_b, _| { + assert_collaborators( + buffer_a.collaborators(), + &[client_a.user_id(), client_b.user_id()], + ); + assert_eq!(buffer_a.collaborators(), buffer_b.collaborators()); + }); + }); +} + +#[gpui::test(iterations = 10)] +async fn test_following_to_channel_notes_without_a_shared_project( + deterministic: BackgroundExecutor, + mut cx_a: &mut TestAppContext, + mut cx_b: &mut TestAppContext, + mut cx_c: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let client_c = server.create_client(cx_c, "user_c").await; + + cx_a.update(editor::init); + cx_b.update(editor::init); + cx_c.update(editor::init); + cx_a.update(collab_ui::channel_view::init); + cx_b.update(collab_ui::channel_view::init); + cx_c.update(collab_ui::channel_view::init); + + let channel_1_id = server + .make_channel( + "channel-1", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + let channel_2_id = server + .make_channel( + "channel-2", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b), (&client_c, cx_c)], + ) + .await; + + // Clients A, B, and C join a channel. + let active_call_a = cx_a.read(ActiveCall::global); + let active_call_b = cx_b.read(ActiveCall::global); + let active_call_c = cx_c.read(ActiveCall::global); + for (call, cx) in [ + (&active_call_a, &mut cx_a), + (&active_call_b, &mut cx_b), + (&active_call_c, &mut cx_c), + ] { + call.update(*cx, |call, cx| call.join_channel(channel_1_id, cx)) + .await + .unwrap(); + } + deterministic.run_until_parked(); + + // Clients A, B, and C all open their own unshared projects. + client_a.fs().insert_tree("/a", json!({})).await; + client_b.fs().insert_tree("/b", json!({})).await; + client_c.fs().insert_tree("/c", json!({})).await; + let (project_a, _) = client_a.build_local_project("/a", cx_a).await; + let (project_b, _) = client_b.build_local_project("/b", cx_b).await; + let (project_c, _) = client_b.build_local_project("/c", cx_c).await; + let (workspace_a, cx_a) = client_a.build_workspace(&project_a, cx_a); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let (_workspace_c, _cx_c) = client_c.build_workspace(&project_c, cx_c); + + active_call_a + .update(cx_a, |call, cx| call.set_location(Some(&project_a), cx)) + .await + .unwrap(); + + // Client A opens the notes for channel 1. + let channel_view_1_a = cx_a + .update(|cx| ChannelView::open(channel_1_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_1_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); + notes.editor.update(cx, |editor, cx| { + editor.insert("Hello from A.", cx); + editor.change_selections(None, cx, |selections| { + selections.select_ranges(vec![3..4]); + }); + }); + }); + + // Client B follows client A. + workspace_b + .update(cx_b, |workspace, cx| { + workspace.follow(client_a.peer_id().unwrap(), cx).unwrap() + }) + .await + .unwrap(); + + // Client B is taken to the notes for channel 1, with the same + // text selected as client A. + deterministic.run_until_parked(); + let channel_view_1_b = workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_1_b.update(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-1"); + let editor = notes.editor.read(cx); + assert_eq!(editor.text(cx), "Hello from A."); + assert_eq!(editor.selections.ranges::(cx), &[3..4]); + }); + + // Client A opens the notes for channel 2. + eprintln!("opening -------------------->"); + + let channel_view_2_a = cx_a + .update(|cx| ChannelView::open(channel_2_id, workspace_a.clone(), cx)) + .await + .unwrap(); + channel_view_2_a.update(cx_a, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + }); + + // Client B is taken to the notes for channel 2. + deterministic.run_until_parked(); + + eprintln!("opening <--------------------"); + + let channel_view_2_b = workspace_b.update(cx_b, |workspace, cx| { + assert_eq!( + workspace.leader_for_pane(workspace.active_pane()), + Some(client_a.peer_id().unwrap()) + ); + workspace + .active_item(cx) + .expect("no active item") + .downcast::() + .expect("active item is not a channel view") + }); + channel_view_2_b.update(cx_b, |notes, cx| { + assert_eq!(notes.channel(cx).unwrap().name, "channel-2"); + }); +} + +#[gpui::test] +async fn test_channel_buffer_changes( + deterministic: BackgroundExecutor, + cx_a: &mut TestAppContext, + cx_b: &mut TestAppContext, +) { + let mut server = TestServer::start(deterministic.clone()).await; + let client_a = server.create_client(cx_a, "user_a").await; + let client_b = server.create_client(cx_b, "user_b").await; + + let channel_id = server + .make_channel( + "the-channel", + None, + (&client_a, cx_a), + &mut [(&client_b, cx_b)], + ) + .await; + + let channel_buffer_a = client_a + .channel_store() + .update(cx_a, |store, cx| store.open_channel_buffer(channel_id, cx)) + .await + .unwrap(); + + // Client A makes an edit, and client B should see that the note has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "1")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); + + // Opening the buffer should clear the changed flag. + let project_b = client_b.build_empty_local_project(cx_b); + let (workspace_b, cx_b) = client_b.build_workspace(&project_b, cx_b); + let channel_view_b = cx_b + .update(|cx| ChannelView::open(channel_id, workspace_b.clone(), cx)) + .await + .unwrap(); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.update(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Editing the channel while the buffer is open should not show that the buffer has changed. + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "2")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + deterministic.advance_clock(ACKNOWLEDGE_DEBOUNCE_INTERVAL); + + // Test that the server is tracking things correctly, and we retain our 'not changed' + // state across a disconnect + server + .simulate_long_connection_interruption(client_b.peer_id().unwrap(), deterministic.clone()); + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(!has_buffer_changed); + + // Closing the buffer should re-enable change tracking + cx_b.update(|cx| { + workspace_b.update(cx, |workspace, cx| { + workspace.close_all_items_and_panes(&Default::default(), cx) + }); + + drop(channel_view_b) + }); + + deterministic.run_until_parked(); + + channel_buffer_a.update(cx_a, |buffer, cx| { + buffer.buffer().update(cx, |buffer, cx| { + buffer.edit([(0..0, "3")], None, cx); + }) + }); + deterministic.run_until_parked(); + + let has_buffer_changed = cx_b.read(|cx| { + client_b + .channel_store() + .read(cx) + .has_channel_buffer_changed(channel_id) + .unwrap() + }); + assert!(has_buffer_changed); +} + +#[track_caller] +fn assert_collaborators(collaborators: &HashMap, ids: &[Option]) { + let mut user_ids = collaborators + .values() + .map(|collaborator| collaborator.user_id) + .collect::>(); + user_ids.sort(); + assert_eq!( + user_ids, + ids.into_iter().map(|id| id.unwrap()).collect::>() + ); +} + +fn buffer_text(channel_buffer: &Model, cx: &mut TestAppContext) -> String { + channel_buffer.read_with(cx, |buffer, _| buffer.text()) +} diff --git a/crates/collab2/src/tests/test_server.rs b/crates/collab2/src/tests/test_server.rs index 6bb57e11ab1d582031930f34b8bfe67b96a2581e..f7517369711d6fc53fc5c53041122134af797e72 100644 --- a/crates/collab2/src/tests/test_server.rs +++ b/crates/collab2/src/tests/test_server.rs @@ -13,7 +13,7 @@ use client::{ use collections::{HashMap, HashSet}; use fs::FakeFs; use futures::{channel::oneshot, StreamExt as _}; -use gpui::{BackgroundExecutor, Context, Model, TestAppContext, WindowHandle}; +use gpui::{BackgroundExecutor, Context, Model, TestAppContext, View, VisualTestContext}; use language::LanguageRegistry; use node_runtime::FakeNodeRuntime; @@ -602,14 +602,12 @@ impl TestClient { .unwrap() } - //todo(workspace) - #[allow(dead_code)] - pub fn build_workspace( - &self, + pub fn build_workspace<'a>( + &'a self, project: &Model, - cx: &mut TestAppContext, - ) -> WindowHandle { - cx.add_window(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) + cx: &'a mut TestAppContext, + ) -> (View, &'a mut VisualTestContext) { + cx.add_window_view(|cx| Workspace::new(0, project.clone(), self.app_state.clone(), cx)) } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index cbd70e52ffab4d539abfe6a177f8a112825b8402..9f0c7e6aca01a934277f2f11f22bee90c4d68d36 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -545,6 +545,10 @@ pub struct VisualTestContext<'a> { } impl<'a> VisualTestContext<'a> { + pub fn update(&mut self, f: impl FnOnce(&mut WindowContext) -> R) -> R { + self.cx.update_window(self.window, |_, cx| f(cx)).unwrap() + } + pub fn from_window(window: AnyWindowHandle, cx: &'a mut TestAppContext) -> Self { Self { cx, window } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 9c886839c93811f5b6668c1e6ff60ae6875a8c14..d5583be0bceb409944d2f7fb1c473c3900f67938 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -2077,6 +2077,7 @@ impl Workspace { } if &pane == self.active_pane() { self.active_item_path_changed(cx); + self.update_active_view_for_followers(cx); } } pane::Event::ChangeItemTitle => { @@ -2756,18 +2757,18 @@ impl Workspace { fn update_active_view_for_followers(&mut self, cx: &mut ViewContext) { let mut is_project_item = true; let mut update = proto::UpdateActiveView::default(); - if self.active_pane.read(cx).has_focus(cx) { - let item = self - .active_item(cx) - .and_then(|item| item.to_followable_item_handle(cx)); - if let Some(item) = item { - is_project_item = item.is_project_item(cx); - update = proto::UpdateActiveView { - id: item - .remote_id(&self.app_state.client, cx) - .map(|id| id.to_proto()), - leader_id: self.leader_for_pane(&self.active_pane), - }; + + if let Some(item) = self.active_item(cx) { + if item.focus_handle(cx).contains_focused(cx) { + if let Some(item) = item.to_followable_item_handle(cx) { + is_project_item = item.is_project_item(cx); + update = proto::UpdateActiveView { + id: item + .remote_id(&self.app_state.client, cx) + .map(|id| id.to_proto()), + leader_id: self.leader_for_pane(&self.active_pane), + }; + } } } From b94c335605c334c24221d53f04ba5af46d304a44 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 6 Dec 2023 22:19:48 +0200 Subject: [PATCH 77/90] Do not run the same workflow concurrently on non-main branches --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8ac2912424bb8bf10014ac1b8961405cd2ba47c5..5ba25dbf947c14b485e7303cd12dd17f8a206927 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,11 @@ on: branches: - "**" +concurrency: + # Allow only one workflow per any non-`main` branch. + group: ${{ github.workflow }}-${{ github.ref_name }}-${{ github.ref_name == 'main' && github.sha || 'anysha' }} + cancel-in-progress: true + env: CARGO_TERM_COLOR: always CARGO_INCREMENTAL: 0 From d711087529938df85fadd7b3c2c472954cf077b4 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 15:44:50 -0500 Subject: [PATCH 78/90] Wire up inline assist quick action --- Cargo.lock | 1 + crates/quick_action_bar2/Cargo.toml | 2 +- .../quick_action_bar2/src/quick_action_bar.rs | 23 +++++++++++-------- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b5af36a19b8fcf1049e7d762db189a50730359a..6eefc3309d76e4c4a078af40f7a0a2e0ae76bbeb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7117,6 +7117,7 @@ dependencies = [ name = "quick_action_bar2" version = "0.1.0" dependencies = [ + "assistant2", "editor2", "gpui2", "search2", diff --git a/crates/quick_action_bar2/Cargo.toml b/crates/quick_action_bar2/Cargo.toml index 32f440d202648b5c0dba8071d7aa5e49d4da18db..678e5c16f1cbbb26ee5ab39113e1a1a8d572ea50 100644 --- a/crates/quick_action_bar2/Cargo.toml +++ b/crates/quick_action_bar2/Cargo.toml @@ -9,7 +9,7 @@ path = "src/quick_action_bar.rs" doctest = false [dependencies] -#assistant = { path = "../assistant" } +assistant = { package = "assistant2", path = "../assistant2" } editor = { package = "editor2", path = "../editor2" } gpui = { package = "gpui2", path = "../gpui2" } search = { package = "search2", path = "../search2" } diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 3686ace2fb0b73a530a98681a5639c3a1bfc71e4..91e0f54e7fe3f04deb2540e1e59c8aac38a8fdea 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -1,4 +1,4 @@ -// use assistant::{assistant_panel::InlineAssist, AssistantPanel}; +use assistant::{AssistantPanel, InlineAssist}; use editor::Editor; use gpui::{ @@ -15,7 +15,6 @@ pub struct QuickActionBar { buffer_search_bar: View, active_item: Option>, _inlay_hints_enabled_subscription: Option, - #[allow(unused)] workspace: WeakView, } @@ -56,21 +55,25 @@ impl Render for QuickActionBar { "toggle inline assistant", Icon::MagicWand, false, - Box::new(gpui::NoAction), + Box::new(InlineAssist), "Inline assistant", - |_, _cx| todo!(), + { + let workspace = self.workspace.clone(); + move |_, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + AssistantPanel::inline_assist(workspace, &InlineAssist, cx); + }); + } + } + }, ); h_stack() .id("quick action bar") .p_1() .gap_2() .child(search_button) - .child( - div() - .border() - .border_color(gpui::red()) - .child(assistant_button), - ) + .child(assistant_button) } } From dbb501d7eb76e21d25f2694730be2b564895f3ac Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 15:45:03 -0500 Subject: [PATCH 79/90] Add gap between label and keybinding in `Tooltip` --- crates/ui2/src/components/tooltip.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index cc17a610f4bd532cfc25669352643e1b0dc6a648..7c502ac5cb13a4322db626602feedcd34edccdce 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -84,6 +84,7 @@ impl Render for Tooltip { .px_2() .child( h_stack() + .gap_2() .child(self.title.clone()) .when_some(self.key_binding.clone(), |this, key_binding| { this.justify_between().child(key_binding) From f7c995c4a08d56eb5c18d8e6d3ef2fa7e579dd62 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 16:14:39 -0500 Subject: [PATCH 80/90] Add "Toggle Inlay Hints" quick action (#3520) This PR adds the "Toggle Inlay Hints" quick action to the toolbar. Release Notes: - N/A --- .../quick_action_bar/src/quick_action_bar.rs | 8 +- .../quick_action_bar2/src/quick_action_bar.rs | 173 ++++-------------- 2 files changed, 39 insertions(+), 142 deletions(-) diff --git a/crates/quick_action_bar/src/quick_action_bar.rs b/crates/quick_action_bar/src/quick_action_bar.rs index d648e83f8f1acbfe013ab411be89ca1bcd5380fe..074ef7d2f398ba557e7e101292a7d7cbb012eb94 100644 --- a/crates/quick_action_bar/src/quick_action_bar.rs +++ b/crates/quick_action_bar/src/quick_action_bar.rs @@ -13,7 +13,7 @@ use workspace::{item::ItemHandle, ToolbarItemLocation, ToolbarItemView, Workspac pub struct QuickActionBar { buffer_search_bar: ViewHandle, active_item: Option>, - _inlay_hints_enabled_subscription: Option, + inlay_hints_enabled_subscription: Option, workspace: WeakViewHandle, } @@ -22,7 +22,7 @@ impl QuickActionBar { Self { buffer_search_bar, active_item: None, - _inlay_hints_enabled_subscription: None, + inlay_hints_enabled_subscription: None, workspace: workspace.weak_handle(), } } @@ -161,12 +161,12 @@ impl ToolbarItemView for QuickActionBar { match active_pane_item { Some(active_item) => { self.active_item = Some(active_item.boxed_clone()); - self._inlay_hints_enabled_subscription.take(); + self.inlay_hints_enabled_subscription.take(); if let Some(editor) = active_item.downcast::() { let mut inlay_hints_enabled = editor.read(cx).inlay_hints_enabled(); let mut supports_inlay_hints = editor.read(cx).supports_inlay_hints(cx); - self._inlay_hints_enabled_subscription = + self.inlay_hints_enabled_subscription = Some(cx.observe(&editor, move |_, editor, cx| { let editor = editor.read(cx); let new_inlay_hints_enabled = editor.inlay_hints_enabled(); diff --git a/crates/quick_action_bar2/src/quick_action_bar.rs b/crates/quick_action_bar2/src/quick_action_bar.rs index 91e0f54e7fe3f04deb2540e1e59c8aac38a8fdea..e933689e62ee2f59e49aeeaf080032c489702fe6 100644 --- a/crates/quick_action_bar2/src/quick_action_bar.rs +++ b/crates/quick_action_bar2/src/quick_action_bar.rs @@ -28,7 +28,6 @@ impl QuickActionBar { } } - #[allow(dead_code)] fn active_editor(&self) -> Option> { self.active_item .as_ref() @@ -40,23 +39,48 @@ impl Render for QuickActionBar { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let buffer_search_bar = self.buffer_search_bar.clone(); - let search_button = QuickActionBarButton::new( + let Some(editor) = self.active_editor() else { + return div().id("empty quick action bar"); + }; + + let inlay_hints_button = Some(QuickActionBarButton::new( + "toggle inlay hints", + Icon::InlayHint, + editor.read(cx).inlay_hints_enabled(), + Box::new(editor::ToggleInlayHints), + "Toggle Inlay Hints", + { + let editor = editor.clone(); + move |_, cx| { + editor.update(cx, |editor, cx| { + editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); + }); + } + }, + )) + .filter(|_| editor.read(cx).supports_inlay_hints(cx)); + + let search_button = Some(QuickActionBarButton::new( "toggle buffer search", Icon::MagnifyingGlass, !self.buffer_search_bar.read(cx).is_dismissed(), Box::new(search::buffer_search::Deploy { focus: false }), "Buffer Search", - move |_, cx| { - buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx)); + { + let buffer_search_bar = self.buffer_search_bar.clone(); + move |_, cx| { + buffer_search_bar.update(cx, |search_bar, cx| search_bar.toggle(cx)); + } }, - ); + )) + .filter(|_| editor.is_singleton(cx)); + let assistant_button = QuickActionBarButton::new( "toggle inline assistant", Icon::MagicWand, false, Box::new(InlineAssist), - "Inline assistant", + "Inline Assist", { let workspace = self.workspace.clone(); move |_, cx| { @@ -68,92 +92,19 @@ impl Render for QuickActionBar { } }, ); + h_stack() .id("quick action bar") .p_1() .gap_2() - .child(search_button) + .children(inlay_hints_button) + .children(search_button) .child(assistant_button) } } impl EventEmitter for QuickActionBar {} -// impl View for QuickActionBar { -// fn ui_name() -> &'static str { -// "QuickActionsBar" -// } - -// fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { -// let Some(editor) = self.active_editor() else { -// return div(); -// }; - -// let mut bar = Flex::row(); -// if editor.read(cx).supports_inlay_hints(cx) { -// bar = bar.with_child(render_quick_action_bar_button( -// 0, -// "icons/inlay_hint.svg", -// editor.read(cx).inlay_hints_enabled(), -// ( -// "Toggle Inlay Hints".to_string(), -// Some(Box::new(editor::ToggleInlayHints)), -// ), -// cx, -// |this, cx| { -// if let Some(editor) = this.active_editor() { -// editor.update(cx, |editor, cx| { -// editor.toggle_inlay_hints(&editor::ToggleInlayHints, cx); -// }); -// } -// }, -// )); -// } - -// if editor.read(cx).buffer().read(cx).is_singleton() { -// let search_bar_shown = !self.buffer_search_bar.read(cx).is_dismissed(); -// let search_action = buffer_search::Deploy { focus: true }; - -// bar = bar.with_child(render_quick_action_bar_button( -// 1, -// "icons/magnifying_glass.svg", -// search_bar_shown, -// ( -// "Buffer Search".to_string(), -// Some(Box::new(search_action.clone())), -// ), -// cx, -// move |this, cx| { -// this.buffer_search_bar.update(cx, |buffer_search_bar, cx| { -// if search_bar_shown { -// buffer_search_bar.dismiss(&buffer_search::Dismiss, cx); -// } else { -// buffer_search_bar.deploy(&search_action, cx); -// } -// }); -// }, -// )); -// } - -// bar.add_child(render_quick_action_bar_button( -// 2, -// "icons/magic-wand.svg", -// false, -// ("Inline Assist".into(), Some(Box::new(InlineAssist))), -// cx, -// move |this, cx| { -// if let Some(workspace) = this.workspace.upgrade(cx) { -// workspace.update(cx, |workspace, cx| { -// AssistantPanel::inline_assist(workspace, &Default::default(), cx); -// }); -// } -// }, -// )); - -// bar.into_any() -// } -// } - #[derive(IntoElement)] struct QuickActionBarButton { id: ElementId, @@ -161,7 +112,6 @@ struct QuickActionBarButton { toggled: bool, action: Box, tooltip: SharedString, - tooltip_meta: Option, on_click: Box, } @@ -180,16 +130,9 @@ impl QuickActionBarButton { toggled, action, tooltip: tooltip.into(), - tooltip_meta: None, on_click: Box::new(on_click), } } - - #[allow(dead_code)] - pub fn meta(mut self, meta: Option>) -> Self { - self.tooltip_meta = meta.map(|meta| meta.into()); - self - } } impl RenderOnce for QuickActionBarButton { @@ -198,63 +141,17 @@ impl RenderOnce for QuickActionBarButton { fn render(self, _: &mut WindowContext) -> Self::Rendered { let tooltip = self.tooltip.clone(); let action = self.action.boxed_clone(); - let tooltip_meta = self.tooltip_meta.clone(); IconButton::new(self.id.clone(), self.icon) .size(ButtonSize::Compact) .icon_size(IconSize::Small) .style(ButtonStyle::Subtle) .selected(self.toggled) - .tooltip(move |cx| { - if let Some(meta) = &tooltip_meta { - Tooltip::with_meta(tooltip.clone(), Some(&*action), meta.clone(), cx) - } else { - Tooltip::for_action(tooltip.clone(), &*action, cx) - } - }) + .tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx)) .on_click(move |event, cx| (self.on_click)(event, cx)) } } -// fn render_quick_action_bar_button< -// F: 'static + Fn(&mut QuickActionBar, &mut ViewContext), -// >( -// index: usize, -// icon: &'static str, -// toggled: bool, -// tooltip: (String, Option>), -// cx: &mut ViewContext, -// on_click: F, -// ) -> AnyElement { -// enum QuickActionBarButton {} - -// let theme = theme::current(cx); -// let (tooltip_text, action) = tooltip; - -// MouseEventHandler::new::(index, cx, |mouse_state, _| { -// let style = theme -// .workspace -// .toolbar -// .toggleable_tool -// .in_state(toggled) -// .style_for(mouse_state); -// Svg::new(icon) -// .with_color(style.color) -// .constrained() -// .with_width(style.icon_width) -// .aligned() -// .constrained() -// .with_width(style.button_width) -// .with_height(style.button_width) -// .contained() -// .with_style(style.container) -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)) -// .with_tooltip::(index, tooltip_text, action, theme.tooltip.clone(), cx) -// .into_any_named("quick action bar button") -// } - impl ToolbarItemView for QuickActionBar { fn set_active_pane_item( &mut self, From 41fc30f62e733b11c863c52ce15586d96146f9d5 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 16:29:35 -0500 Subject: [PATCH 81/90] Always show tooltips on buttons (#3521) This PR fixes an issue where tooltips weren't being shown on selected buttons. We now always show tooltips on buttons that have one. Release Notes: - N/A --- crates/ui2/src/components/button/button_like.rs | 6 +----- crates/ui2/src/components/stories/icon_button.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/ui2/src/components/button/button_like.rs b/crates/ui2/src/components/button/button_like.rs index 4bef6bff774571047dd1519c18b7ec61b32f1904..1a33eb2845eef9e767181bb2cc5f65ba7eb8957f 100644 --- a/crates/ui2/src/components/button/button_like.rs +++ b/crates/ui2/src/components/button/button_like.rs @@ -359,11 +359,7 @@ impl RenderOnce for ButtonLike { }, ) .when_some(self.tooltip, |this, tooltip| { - if !self.selected { - this.tooltip(move |cx| tooltip(cx)) - } else { - this - } + this.tooltip(move |cx| tooltip(cx)) }) .children(self.children) } diff --git a/crates/ui2/src/components/stories/icon_button.rs b/crates/ui2/src/components/stories/icon_button.rs index 3c4d68f8aff5500225be0f6cf83c096a5c6c6dff..583f453d188b9df5aaaf71b5bb1897814c0a6f51 100644 --- a/crates/ui2/src/components/stories/icon_button.rs +++ b/crates/ui2/src/components/stories/icon_button.rs @@ -51,5 +51,13 @@ impl Render for IconButtonStory { .tooltip(|cx| Tooltip::text("Open messages", cx)), ), ) + .child(Story::label("Selected with `tooltip`")) + .child( + div().w_8().child( + IconButton::new("selected_with_tooltip", Icon::InlayHint) + .selected(true) + .tooltip(|cx| Tooltip::text("Toggle inlay hints", cx)), + ), + ) } } From 89c8a7c2426a426fdda1af36e8121d77a01cc128 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 13:52:33 -0800 Subject: [PATCH 82/90] Enable buffer font size adjustment in zed2 Co-authored-by: Nathan --- crates/editor2/src/editor.rs | 2 +- crates/gpui2/src/app.rs | 1 - crates/theme2/src/settings.rs | 31 +++++++++++++++---------------- crates/ui2/src/styled_ext.rs | 3 +-- crates/zed2/src/zed2.rs | 15 +++++++-------- 5 files changed, 24 insertions(+), 28 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 94ae8abc71a2d99ff2b5bde104809a824cab3877..8f9d22e6c9e6f7b4277d089e5be572f15415d76b 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9291,7 +9291,7 @@ impl Render for Editor { color: cx.theme().colors().text, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features, - font_size: settings.buffer_font_size.into(), + font_size: settings.buffer_font_size(cx).into(), font_weight: FontWeight::NORMAL, font_style: FontStyle::Normal, line_height: relative(settings.buffer_line_height.value()), diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 0715ace9eaf8ca0de7fabcb635264e361fc09dad..9293302938c13b8948fab22dd4fc092e69da6c80 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -860,7 +860,6 @@ impl AppContext { } /// Remove the global of the given type from the app context. Does not notify global observers. - #[cfg(any(test, feature = "test-support"))] pub fn remove_global(&mut self) -> G { let global_type = TypeId::of::(); *self diff --git a/crates/theme2/src/settings.rs b/crates/theme2/src/settings.rs index 15b578d4b0e10855d0368ce9d31167847ae8944b..624b14fe33fe194e2f8b7af3211ff4c9a144c100 100644 --- a/crates/theme2/src/settings.rs +++ b/crates/theme2/src/settings.rs @@ -27,7 +27,7 @@ pub struct ThemeSettings { } #[derive(Default)] -pub struct AdjustedBufferFontSize(Option); +pub struct AdjustedBufferFontSize(Pixels); #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct ThemeSettingsContent { @@ -69,12 +69,10 @@ impl BufferLineHeight { } impl ThemeSettings { - pub fn buffer_font_size(&self, cx: &mut AppContext) -> Pixels { - let font_size = *cx - .default_global::() - .0 - .get_or_insert(self.buffer_font_size.into()); - font_size.max(MIN_FONT_SIZE) + pub fn buffer_font_size(&self, cx: &AppContext) -> Pixels { + cx.try_global::() + .map_or(self.buffer_font_size, |size| size.0) + .max(MIN_FONT_SIZE) } pub fn line_height(&self) -> f32 { @@ -83,9 +81,9 @@ impl ThemeSettings { } pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { - if let Some(adjusted_size) = cx.default_global::().0 { + if let Some(AdjustedBufferFontSize(adjusted_size)) = cx.try_global::() { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let delta = adjusted_size - buffer_font_size; + let delta = *adjusted_size - buffer_font_size; size + delta } else { size @@ -95,18 +93,19 @@ pub fn adjusted_font_size(size: Pixels, cx: &mut AppContext) -> Pixels { pub fn adjust_font_size(cx: &mut AppContext, f: fn(&mut Pixels)) { let buffer_font_size = ThemeSettings::get_global(cx).buffer_font_size; - let adjusted_size = cx - .default_global::() - .0 - .get_or_insert(buffer_font_size); - f(adjusted_size); - *adjusted_size = (*adjusted_size).max(MIN_FONT_SIZE - buffer_font_size); + let mut adjusted_size = cx + .try_global::() + .map_or(buffer_font_size, |adjusted_size| adjusted_size.0); + + f(&mut adjusted_size); + adjusted_size = adjusted_size.max(MIN_FONT_SIZE); + cx.set_global(AdjustedBufferFontSize(adjusted_size)); cx.refresh(); } pub fn reset_font_size(cx: &mut AppContext) { if cx.has_global::() { - cx.global_mut::().0 = None; + cx.remove_global::(); cx.refresh(); } } diff --git a/crates/ui2/src/styled_ext.rs b/crates/ui2/src/styled_ext.rs index e567830d6ca9ddbc978d0bd08b43ab404625ae9b..0f882b496346252226f8224397c65340b6532280 100644 --- a/crates/ui2/src/styled_ext.rs +++ b/crates/ui2/src/styled_ext.rs @@ -70,8 +70,7 @@ pub trait StyledExt: Styled + Sized { /// or other places that text needs to match the user's buffer font size. fn text_buffer(self, cx: &mut WindowContext) -> Self { let settings = ThemeSettings::get_global(cx); - - self.text_size(settings.buffer_font_size) + self.text_size(settings.buffer_font_size(cx)) } /// The [`Surface`](ui2::ElevationIndex::Surface) elevation level, located above the app background, is the standard level for all elements diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index d220250b3dba8bdd55bc0be98e85a62565902225..45ace75ebcc16c3dadc6a639699ebde401a0139f 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -235,14 +235,13 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { .open_urls(&[action.url.clone()]) }) .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) - //todo!(buffer font size) - // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size += 1.0) - // }); - // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size -= 1.0) - // }); - // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + .register_action(move |_, _: &IncreaseBufferFontSize, cx| { + theme::adjust_font_size(cx, |size| *size += px(1.0)) + }) + .register_action(move |_, _: &DecreaseBufferFontSize, cx| { + theme::adjust_font_size(cx, |size| *size -= px(1.0)) + }) + .register_action(move |_, _: &ResetBufferFontSize, cx| theme::reset_font_size(cx)) .register_action(|_, _: &install_cli::Install, cx| { cx.spawn(|_, cx| async move { install_cli::install_cli(cx.deref()) From b72c54fc311c7bd56f6aab2eb6212a2d43a51afe Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 16:59:24 -0500 Subject: [PATCH 83/90] Don't load the Vim keymap temporarily (#3522) This PR removes the loading of the Vim keymap temporarily. This cuts down on the noise from all of the Vim-related action warnings. We can resume loading the Vim keymap once we're ready to add Vim support. Release Notes: - N/A --- crates/settings2/src/settings_file.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/settings2/src/settings_file.rs b/crates/settings2/src/settings_file.rs index 590079c51b52fe77a2c83ec4a862b27a0202ad1a..46450a9c282754678d6f7c26bbbc98a4122d82d1 100644 --- a/crates/settings2/src/settings_file.rs +++ b/crates/settings2/src/settings_file.rs @@ -124,6 +124,17 @@ pub fn update_settings_file( pub fn load_default_keymap(cx: &mut AppContext) { for path in ["keymaps/default.json", "keymaps/vim.json"] { + // TODO: Remove this conditional when we're ready to add Vim support. + // Right now we're avoiding loading the Vim keymap to silence the warnings + // about invalid action bindings. + if path.contains("vim") { + let _: Option<()> = Err(format!( + "TODO: Skipping {path} until we're ready to add Vim support" + )) + .log_err(); + continue; + } + KeymapFile::load_asset(path, cx).unwrap(); } From 2d18b949adb16786fb28f8c96c9293eb14f035d6 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 6 Dec 2023 14:29:09 -0800 Subject: [PATCH 84/90] Upgrade async-compression dep --- Cargo.lock | 4 ++-- Cargo.toml | 1 + crates/copilot/Cargo.toml | 2 +- crates/copilot2/Cargo.toml | 2 +- crates/node_runtime/Cargo.toml | 2 +- crates/zed/Cargo.toml | 2 +- crates/zed2/Cargo.toml | 2 +- 7 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c9e1ab3f2b6c1dce78f675db5456c2ab7dbbea5a..59062e5e144a8b9a81b4e66b0d7c505d3ceb2e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -454,9 +454,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.3.15" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" +checksum = "bc2d0cfb2a7388d34f590e76686704c494ed7aaceed62ee1ba35cbf363abc2a5" dependencies = [ "flate2", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index 376e3ee62f9ab3e15a12aa3c8beb736763cbf276..6b154cc87fac27cbb6dc252c2a1099d2667f74b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ resolver = "2" [workspace.dependencies] anyhow = { version = "1.0.57" } async-trait = { version = "0.1" } +async-compression = { version = "0.4", features = ["gzip", "futures-io"] } # TODO: Switch back to the published version of `ctor` once: # 1. A new version of `ctor` is published with this change: https://github.com/mmastrac/rust-ctor/pull/295 # 2. We've confirmed it's fine to update to the latest version of `ctor` (we're currently on v0.1.20). diff --git a/crates/copilot/Cargo.toml b/crates/copilot/Cargo.toml index 2558974753f124c67e404f0065e8ab51fd83367b..985e784367459dc987ae5014a14f9e0e90df8a6c 100644 --- a/crates/copilot/Cargo.toml +++ b/crates/copilot/Cargo.toml @@ -28,7 +28,7 @@ theme = { path = "../theme" } lsp = { path = "../lsp" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" anyhow.workspace = true log.workspace = true diff --git a/crates/copilot2/Cargo.toml b/crates/copilot2/Cargo.toml index b04a7d1246ebfbcbb03dbdf179d2633934cecd98..ce169f3319a935fc061f6f06f53134366950b32f 100644 --- a/crates/copilot2/Cargo.toml +++ b/crates/copilot2/Cargo.toml @@ -29,7 +29,7 @@ lsp = { package = "lsp2", path = "../lsp2" } node_runtime = { path = "../node_runtime"} util = { path = "../util" } ui = { package = "ui2", path = "../ui2" } -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" anyhow.workspace = true log.workspace = true diff --git a/crates/node_runtime/Cargo.toml b/crates/node_runtime/Cargo.toml index faa837fb67062a8b1de7a1f4eabd3baae8a71665..0b4e7a262d9b2bb095f4772b57acf1cc31bbde0a 100644 --- a/crates/node_runtime/Cargo.toml +++ b/crates/node_runtime/Cargo.toml @@ -10,7 +10,7 @@ doctest = false [dependencies] util = { path = "../util" } -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" futures.workspace = true async-trait.workspace = true diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 6d9cb3c7502bc539cedc26d45c97fde5e5b7099f..f665cc36dbdc8f4b0fa8576768efb32388d07374 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -78,7 +78,7 @@ workspace = { path = "../workspace" } welcome = { path = "../welcome" } zed-actions = {path = "../zed-actions"} anyhow.workspace = true -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" async-recursion = "0.3" async-trait.workspace = true diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 0fcbcc40fc281b45ad304d48149aec89506c8275..427e72068e755d61161920c35652fa376a45b169 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -74,7 +74,7 @@ workspace = { package = "workspace2", path = "../workspace2" } welcome = { package = "welcome2", path = "../welcome2" } zed_actions = {package = "zed_actions2", path = "../zed_actions2"} anyhow.workspace = true -async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } +async-compression.workspace = true async-tar = "0.4.2" async-recursion = "0.3" async-trait.workspace = true From 12e7f61f625f6f9690d88b05fa082dab075b6291 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 6 Dec 2023 15:07:09 -0800 Subject: [PATCH 85/90] Fix a porting bugs for terminal2 co-authored-by: Nathan --- crates/editor2/src/element.rs | 1 - crates/gpui2/src/elements/div.rs | 26 ------------ crates/gpui2/src/platform/test/window.rs | 26 ++++++------ crates/terminal2/src/mappings/mouse.rs | 4 +- crates/terminal_view2/src/terminal_element.rs | 40 +++++++++---------- crates/terminal_view2/src/terminal_panel.rs | 6 +-- 6 files changed, 36 insertions(+), 67 deletions(-) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index d7b9d0bb40498cd8fb6c51f4cd33d5e6489f4ad1..c9746f1808dfdcab37331edd8044eb1ccc409639 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -3448,7 +3448,6 @@ mod tests { DisplayPoint::new(4, 0)..DisplayPoint::new(6, 0) ); assert_eq!(local_selections[0].head, DisplayPoint::new(5, 0)); - dbg!("Hi"); // moves cursor on buffer boundary back two lines // and doesn't allow selection to bleed through assert_eq!( diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 168be4d094baece74959ffd8306eedbb77ebd030..e4a6daf3908513434e01a209fbec8d7de7341df0 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -95,32 +95,6 @@ pub trait InteractiveElement: Sized + Element { self } - fn on_mouse_down_weird( - mut self, - button: MouseButton, - listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, - ) -> Self { - self.interactivity().mouse_down_listeners.push(Box::new( - move |event, bounds, phase, cx| { - dbg!("HEREEEE"); - - let contains = dbg!(dbg!(&bounds.bounds).contains_point(dbg!(&event.position))) - && dbg!(cx.was_top_layer(&event.position, &bounds.stacking_order)); - dbg!(contains); - - if phase == DispatchPhase::Bubble - && event.button == button - && bounds.visibly_contains(&event.position, cx) - { - dbg!("HEREEEE2"); - - (listener)(event, cx) - } - }, - )); - self - } - fn on_any_mouse_down( mut self, listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static, diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index b1bfebad06745f51899421707d2ead4da57bfcb6..f18722d88d9bd2ab95aa7a89f143e2f7571bfb68 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -66,11 +66,11 @@ impl PlatformWindow for TestWindow { } fn titlebar_height(&self) -> Pixels { - todo!() + unimplemented!() } fn appearance(&self) -> WindowAppearance { - todo!() + unimplemented!() } fn display(&self) -> std::rc::Rc { @@ -99,7 +99,7 @@ impl PlatformWindow for TestWindow { } fn activate(&self) { - todo!() + unimplemented!() } fn set_title(&mut self, title: &str) { @@ -107,23 +107,23 @@ impl PlatformWindow for TestWindow { } fn set_edited(&mut self, _edited: bool) { - todo!() + unimplemented!() } fn show_character_palette(&self) { - todo!() + unimplemented!() } fn minimize(&self) { - todo!() + unimplemented!() } fn zoom(&self) { - todo!() + unimplemented!() } fn toggle_full_screen(&self) { - todo!() + unimplemented!() } fn on_input(&self, callback: Box bool>) { @@ -139,7 +139,7 @@ impl PlatformWindow for TestWindow { } fn on_fullscreen(&self, _callback: Box) { - todo!() + unimplemented!() } fn on_moved(&self, callback: Box) { @@ -147,19 +147,19 @@ impl PlatformWindow for TestWindow { } fn on_should_close(&self, _callback: Box bool>) { - todo!() + unimplemented!() } fn on_close(&self, _callback: Box) { - todo!() + unimplemented!() } fn on_appearance_changed(&self, _callback: Box) { - todo!() + unimplemented!() } fn is_topmost_for_position(&self, _position: crate::Point) -> bool { - todo!() + unimplemented!() } fn draw(&self, scene: crate::Scene) { diff --git a/crates/terminal2/src/mappings/mouse.rs b/crates/terminal2/src/mappings/mouse.rs index edced3156faf90cdf9dc227ee646f0e4cccb68e3..a32d83d28ddb99416cff22a85322184acabb2ac9 100644 --- a/crates/terminal2/src/mappings/mouse.rs +++ b/crates/terminal2/src/mappings/mouse.rs @@ -186,9 +186,9 @@ pub fn mouse_side( } pub fn grid_point(pos: Point, cur_size: TerminalSize, display_offset: usize) -> AlacPoint { - let col = GridCol((cur_size.cell_width / pos.x) as usize); + let col = GridCol((pos.x / cur_size.cell_width) as usize); let col = min(col, cur_size.last_column()); - let line = (cur_size.line_height / pos.y) as i32; + let line = (pos.y / cur_size.line_height) as i32; let line = min(line, cur_size.bottommost_line().0); AlacPoint::new(GridLine(line - display_offset as i32), col) } diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 211d74ea3a8177d07242e096a21d77f463bb4d08..fbd961492c792fdb8675c54ddb22d936651c6f28 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,10 +1,10 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ black, div, point, px, red, relative, transparent_black, AnyElement, AvailableSpace, Bounds, - DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, - Hsla, InteractiveElement, InteractiveElementState, IntoElement, LayoutId, ModelContext, - ModifiersChangedEvent, MouseButton, Pixels, Point, Rgba, ShapedLine, Size, - StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, View, + DispatchPhase, Element, ElementId, ElementInputHandler, FocusHandle, Font, FontStyle, + FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement, + LayoutId, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, Point, Rgba, ShapedLine, + Size, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, View, WeakModel, WhiteSpace, WindowContext, }; use itertools::Itertools; @@ -638,11 +638,10 @@ impl TerminalElement { let connection = self.terminal.clone(); let mut this = self - .on_mouse_down_weird(MouseButton::Left, { + .on_mouse_down(MouseButton::Left, { let connection = connection.clone(); let focus = focus.clone(); move |e, cx| { - dbg!("here"); cx.focus(&focus); //todo!(context menu) // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); @@ -655,18 +654,18 @@ impl TerminalElement { } } }) - .on_drag_event({ + .on_mouse_move({ let connection = connection.clone(); let focus = focus.clone(); move |e, cx| { - dbg!("here"); - - if focus.is_focused(cx) { - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_drag(e, origin, bounds); - cx.notify(); - }) + if e.pressed_button.is_some() { + if focus.is_focused(cx) { + if let Some(conn_handle) = connection.upgrade() { + conn_handle.update(cx, |terminal, cx| { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + }) + } } } } @@ -685,8 +684,6 @@ impl TerminalElement { .on_click({ let connection = connection.clone(); move |e, cx| { - dbg!("here"); - if e.down.button == MouseButton::Right { let mouse_mode = if let Some(conn_handle) = connection.upgrade() { conn_handle.update(cx, |terminal, _cx| { @@ -707,8 +704,6 @@ impl TerminalElement { let connection = connection.clone(); let focus = focus.clone(); move |e, cx| { - dbg!("here"); - if focus.is_focused(cx) { if let Some(conn_handle) = connection.upgrade() { conn_handle.update(cx, |terminal, cx| { @@ -722,8 +717,6 @@ impl TerminalElement { .on_scroll_wheel({ let connection = connection.clone(); move |e, cx| { - dbg!("here"); - if let Some(conn_handle) = connection.upgrade() { conn_handle.update(cx, |terminal, cx| { terminal.scroll_wheel(e, origin); @@ -814,7 +807,6 @@ impl Element for TerminalElement { state: &mut Self::State, cx: &mut WindowContext<'_>, ) { - dbg!(bounds); let mut layout = self.compute_layout(bounds, cx); let theme = cx.theme(); @@ -831,9 +823,13 @@ impl Element for TerminalElement { let origin = bounds.origin + Point::new(layout.gutter, px(0.)); let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx); + let interactivity = mem::take(&mut this.interactivity); interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { + let input_handler = ElementInputHandler::new(bounds, this.terminal_view.clone(), cx); + cx.handle_input(&this.focus, input_handler); + this.register_key_listeners(cx); for rect in &layout.rects { diff --git a/crates/terminal_view2/src/terminal_panel.rs b/crates/terminal_view2/src/terminal_panel.rs index b6582b07b194331f0e33f7e5b6b70557bfa50cbc..4a169836fc23d92e6bd8987f7eabb03aa5a4aad8 100644 --- a/crates/terminal_view2/src/terminal_panel.rs +++ b/crates/terminal_view2/src/terminal_panel.rs @@ -4,8 +4,8 @@ use crate::TerminalView; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, Entity, EventEmitter, - FocusHandle, FocusableView, ParentElement, Render, Subscription, Task, View, ViewContext, - VisualContext, WeakView, WindowContext, + FocusHandle, FocusableView, ParentElement, Render, Styled, Subscription, Task, View, + ViewContext, VisualContext, WeakView, WindowContext, }; use project::Fs; use serde::{Deserialize, Serialize}; @@ -339,7 +339,7 @@ impl Render for TerminalPanel { type Element = Div; fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { - div().child(self.pane.clone()) + div().size_full().child(self.pane.clone()) } } From a4b271e06395b5fe794f88affd0dc08d046628d3 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 18:41:50 -0500 Subject: [PATCH 86/90] Port `recent_projects` to Zed2 (#3525) This PR ports the `recent_projects` crate to Zed2 (`recent_projects2`). Absent from this PR is wiring up the "Recent Projects" item in the title bar. We'll come back to that soon. Release Notes: - N/A --- Cargo.lock | 24 ++ Cargo.toml | 1 + crates/collab_ui2/Cargo.toml | 2 +- crates/recent_projects2/Cargo.toml | 31 +++ .../src/highlighted_workspace_location.rs | 131 ++++++++++ .../recent_projects2/src/recent_projects.rs | 239 ++++++++++++++++++ crates/workspace2/src/workspace2.rs | 3 +- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/app_menus.rs | 2 +- crates/zed2/src/main.rs | 2 +- 10 files changed, 432 insertions(+), 5 deletions(-) create mode 100644 crates/recent_projects2/Cargo.toml create mode 100644 crates/recent_projects2/src/highlighted_workspace_location.rs create mode 100644 crates/recent_projects2/src/recent_projects.rs diff --git a/Cargo.lock b/Cargo.lock index 59062e5e144a8b9a81b4e66b0d7c505d3ceb2e3b..b48fd4dc3d5d99434f94b7a10098f6b8a53b2811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1959,6 +1959,7 @@ dependencies = [ "postage", "pretty_assertions", "project2", + "recent_projects2", "rich_text2", "rpc2", "schemars", @@ -7307,6 +7308,28 @@ dependencies = [ "workspace", ] +[[package]] +name = "recent_projects2" +version = "0.1.0" +dependencies = [ + "db", + "editor2", + "futures 0.3.28", + "fuzzy2", + "gpui2", + "language2", + "ordered-float 2.10.0", + "picker2", + "postage", + "settings2", + "smol", + "text2", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -11948,6 +11971,7 @@ dependencies = [ "project_panel2", "quick_action_bar2", "rand 0.8.5", + "recent_projects2", "regex", "rope2", "rpc2", diff --git a/Cargo.toml b/Cargo.toml index 6b154cc87fac27cbb6dc252c2a1099d2667f74b0..5a3c451fd30025538e960c1b379374bf4bf89bb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,6 +92,7 @@ members = [ "crates/project_symbols", "crates/quick_action_bar2", "crates/recent_projects", + "crates/recent_projects2", "crates/rope", "crates/rpc", "crates/rpc2", diff --git a/crates/collab_ui2/Cargo.toml b/crates/collab_ui2/Cargo.toml index c7c00d7696c567d27b231733c50bd33c0c1e314b..65aced8e7edf1e522a97d44cd4dcbdf04536f348 100644 --- a/crates/collab_ui2/Cargo.toml +++ b/crates/collab_ui2/Cargo.toml @@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" } rich_text = { package = "rich_text2", path = "../rich_text2" } picker = { package = "picker2", path = "../picker2" } project = { package = "project2", path = "../project2" } -# recent_projects = { path = "../recent_projects" } +recent_projects = { package = "recent_projects2", path = "../recent_projects2" } rpc = { package ="rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2"} diff --git a/crates/recent_projects2/Cargo.toml b/crates/recent_projects2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..3d10c147e0ae69ed257d3787618496c904fe823a --- /dev/null +++ b/crates/recent_projects2/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "recent_projects2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/recent_projects.rs" +doctest = false + +[dependencies] +db = { path = "../db" } +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +picker = { package = "picker2", path = "../picker2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } +util = { path = "../util"} +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +workspace = { package = "workspace2", path = "../workspace2" } + +futures.workspace = true +ordered-float.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/recent_projects2/src/highlighted_workspace_location.rs b/crates/recent_projects2/src/highlighted_workspace_location.rs new file mode 100644 index 0000000000000000000000000000000000000000..a4057d2f4bdd090ca46a4f4242bc5b1abdb8e157 --- /dev/null +++ b/crates/recent_projects2/src/highlighted_workspace_location.rs @@ -0,0 +1,131 @@ +use std::path::Path; + +use fuzzy::StringMatch; +use ui::{prelude::*, HighlightedLabel}; +use util::paths::PathExt; +use workspace::WorkspaceLocation; + +#[derive(IntoElement)] +pub struct HighlightedText { + pub text: String, + pub highlight_positions: Vec, + char_count: usize, +} + +impl HighlightedText { + fn join(components: impl Iterator, separator: &str) -> Self { + let mut char_count = 0; + let separator_char_count = separator.chars().count(); + let mut text = String::new(); + let mut highlight_positions = Vec::new(); + for component in components { + if char_count != 0 { + text.push_str(separator); + char_count += separator_char_count; + } + + highlight_positions.extend( + component + .highlight_positions + .iter() + .map(|position| position + char_count), + ); + text.push_str(&component.text); + char_count += component.text.chars().count(); + } + + Self { + text, + highlight_positions, + char_count, + } + } +} + +impl RenderOnce for HighlightedText { + type Rendered = HighlightedLabel; + + fn render(self, _cx: &mut WindowContext) -> Self::Rendered { + HighlightedLabel::new(self.text, self.highlight_positions) + } +} + +pub struct HighlightedWorkspaceLocation { + pub names: HighlightedText, + pub paths: Vec, +} + +impl HighlightedWorkspaceLocation { + pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self { + let mut path_start_offset = 0; + let (names, paths): (Vec<_>, Vec<_>) = location + .paths() + .iter() + .map(|path| { + let path = path.compact(); + let highlighted_text = Self::highlights_for_path( + path.as_ref(), + &string_match.positions, + path_start_offset, + ); + + path_start_offset += highlighted_text.1.char_count; + + highlighted_text + }) + .unzip(); + + Self { + names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "), + paths, + } + } + + // Compute the highlighted text for the name and path + fn highlights_for_path( + path: &Path, + match_positions: &Vec, + path_start_offset: usize, + ) -> (Option, HighlightedText) { + let path_string = path.to_string_lossy(); + let path_char_count = path_string.chars().count(); + // Get the subset of match highlight positions that line up with the given path. + // Also adjusts them to start at the path start + let path_positions = match_positions + .iter() + .copied() + .skip_while(|position| *position < path_start_offset) + .take_while(|position| *position < path_start_offset + path_char_count) + .map(|position| position - path_start_offset) + .collect::>(); + + // Again subset the highlight positions to just those that line up with the file_name + // again adjusted to the start of the file_name + let file_name_text_and_positions = path.file_name().map(|file_name| { + let text = file_name.to_string_lossy(); + let char_count = text.chars().count(); + let file_name_start = path_char_count - char_count; + let highlight_positions = path_positions + .iter() + .copied() + .skip_while(|position| *position < file_name_start) + .take_while(|position| *position < file_name_start + char_count) + .map(|position| position - file_name_start) + .collect::>(); + HighlightedText { + text: text.to_string(), + highlight_positions, + char_count, + } + }); + + ( + file_name_text_and_positions, + HighlightedText { + text: path_string.to_string(), + highlight_positions: path_positions, + char_count: path_char_count, + }, + ) + } +} diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs new file mode 100644 index 0000000000000000000000000000000000000000..03cd042f8207314e5eff53cf873d609e4f61605d --- /dev/null +++ b/crates/recent_projects2/src/recent_projects.rs @@ -0,0 +1,239 @@ +mod highlighted_workspace_location; + +use fuzzy::{StringMatch, StringMatchCandidate}; +use gpui::{ + actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Result, Task, + View, ViewContext, WeakView, +}; +use highlighted_workspace_location::HighlightedWorkspaceLocation; +use ordered_float::OrderedFloat; +use picker::{Picker, PickerDelegate}; +use std::sync::Arc; +use ui::{prelude::*, ListItem}; +use util::paths::PathExt; +use workspace::{ + notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation, + WORKSPACE_DB, +}; + +actions!(OpenRecent); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(RecentProjects::register).detach(); +} + +fn toggle( + _: &mut Workspace, + _: &OpenRecent, + cx: &mut ViewContext, +) -> Option>> { + Some(cx.spawn(|workspace, mut cx| async move { + let workspace_locations: Vec<_> = cx + .background_executor() + .spawn(async { + WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .map(|(_, location)| location) + .collect() + }) + .await; + + workspace.update(&mut cx, |workspace, cx| { + if !workspace_locations.is_empty() { + let weak_workspace = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = + RecentProjectsDelegate::new(weak_workspace, workspace_locations, true); + + RecentProjects::new(delegate, cx) + }); + } else { + workspace.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new("No recent projects to open.")) + }) + } + })?; + Ok(()) + })) +} + +pub struct RecentProjects { + picker: View>, +} + +impl RecentProjects { + fn new(delegate: RecentProjectsDelegate, cx: &mut ViewContext) -> Self { + Self { + picker: cx.build_view(|cx| Picker::new(delegate, cx)), + } + } + + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &OpenRecent, cx| { + let Some(recent_projects) = workspace.active_modal::(cx) else { + // TODO(Marshall): Is this how we should be handling this? + // The previous code was using `cx.add_async_action` to invoke `toggle`. + if let Some(handler) = toggle(workspace, &OpenRecent, cx) { + handler.detach_and_log_err(cx); + } + return; + }; + + recent_projects.update(cx, |recent_projects, cx| { + recent_projects + .picker + .update(cx, |picker, cx| picker.cycle_selection(cx)) + }); + }); + } +} + +impl EventEmitter for RecentProjects {} + +impl FocusableView for RecentProjects { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for RecentProjects { + type Element = Div; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + v_stack().w_96().child(self.picker.clone()) + } +} + +pub struct RecentProjectsDelegate { + workspace: WeakView, + workspace_locations: Vec, + selected_match_index: usize, + matches: Vec, + render_paths: bool, +} + +impl RecentProjectsDelegate { + fn new( + workspace: WeakView, + workspace_locations: Vec, + render_paths: bool, + ) -> Self { + Self { + workspace, + workspace_locations, + selected_match_index: 0, + matches: Default::default(), + render_paths, + } + } +} + +impl PickerDelegate for RecentProjectsDelegate { + type ListItem = ListItem; + + fn placeholder_text(&self) -> Arc { + "Recent Projects...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_match_index + } + + fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { + self.selected_match_index = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let query = query.trim_start(); + let smart_case = query.chars().any(|c| c.is_uppercase()); + let candidates = self + .workspace_locations + .iter() + .enumerate() + .map(|(id, location)| { + let combined_string = location + .paths() + .iter() + .map(|path| path.compact().to_string_lossy().into_owned()) + .collect::>() + .join(""); + StringMatchCandidate::new(id, combined_string) + }) + .collect::>(); + self.matches = smol::block_on(fuzzy::match_strings( + candidates.as_slice(), + query, + smart_case, + 100, + &Default::default(), + cx.background_executor().clone(), + )); + self.matches.sort_unstable_by_key(|m| m.candidate_id); + + self.selected_match_index = self + .matches + .iter() + .enumerate() + .rev() + .max_by_key(|(_, m)| OrderedFloat(m.score)) + .map(|(ix, _)| ix) + .unwrap_or(0); + Task::ready(()) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if let Some((selected_match, workspace)) = self + .matches + .get(self.selected_index()) + .zip(self.workspace.upgrade()) + { + let workspace_location = &self.workspace_locations[selected_match.candidate_id]; + workspace + .update(cx, |workspace, cx| { + workspace + .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx) + }) + .detach_and_log_err(cx); + self.dismissed(cx); + } + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + let Some(r#match) = self.matches.get(ix) else { + return None; + }; + + let highlighted_location = HighlightedWorkspaceLocation::new( + &r#match, + &self.workspace_locations[r#match.candidate_id], + ); + + Some( + ListItem::new(ix).inset(true).selected(selected).child( + v_stack() + .child(highlighted_location.names) + .when(self.render_paths, |this| { + this.children(highlighted_location.paths) + }), + ), + ) + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index d5583be0bceb409944d2f7fb1c473c3900f67938..abf908992978ff8e870eb88efe72f7ce0e332c41 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -45,9 +45,10 @@ use node_runtime::NodeRuntime; use notifications::{simple_message_notification::MessageNotification, NotificationHandle}; pub use pane::*; pub use pane_group::*; +use persistence::DB; pub use persistence::{ model::{ItemId, SerializedWorkspace, WorkspaceLocation}, - WorkspaceDb, DB, + WorkspaceDb, DB as WORKSPACE_DB, }; use postage::stream::Stream; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 427e72068e755d61161920c35652fa376a45b169..e545fe3c97cb0acdda3299dfa1d6b847fcc7aaab 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -56,7 +56,7 @@ project = { package = "project2", path = "../project2" } project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" } -# recent_projects = { path = "../recent_projects" } +recent_projects = { package = "recent_projects2", path = "../recent_projects2" } rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } diff --git a/crates/zed2/src/app_menus.rs b/crates/zed2/src/app_menus.rs index 70b04e8f9be774d3e7d28e610f65bbabfb80c9b0..63db41e7bd5bc0f1a2fefb8fd80e4fc0f9738959 100644 --- a/crates/zed2/src/app_menus.rs +++ b/crates/zed2/src/app_menus.rs @@ -35,7 +35,7 @@ pub fn app_menus() -> Vec> { MenuItem::action("New Window", workspace::NewWindow), MenuItem::separator(), MenuItem::action("Open…", workspace::Open), - // MenuItem::action("Open Recent...", recent_projects::OpenRecent), + MenuItem::action("Open Recent...", recent_projects::OpenRecent), MenuItem::separator(), MenuItem::action("Add Folder to Project…", workspace::AddFolderToProject), MenuItem::action("Save", workspace::Save { save_intent: None }), diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 78a8bdf292d659cc469ccddfb2e1134a1e9eda07..f11c2eaadd6ceaa5b100c844e060feadd69e5edd 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -200,7 +200,7 @@ fn main() { auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); - // recent_projects::init(cx); + recent_projects::init(cx); go_to_line::init(cx); file_finder::init(cx); From c092cfbfb3ae8fcccf2203a7ad595ca81e089f0b Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 6 Dec 2023 15:54:08 -0800 Subject: [PATCH 87/90] Fix bug with IME Adjust how IME works in the terminal co-authored-by: nathan --- crates/gpui2/src/platform.rs | 1 + crates/gpui2/src/platform/mac/window.rs | 4 + crates/gpui2/src/platform/test/window.rs | 4 + crates/gpui2/src/window.rs | 1 + crates/terminal_view2/src/terminal_element.rs | 172 ++++++++++++------ crates/terminal_view2/src/terminal_view.rs | 128 +------------ 6 files changed, 126 insertions(+), 184 deletions(-) diff --git a/crates/gpui2/src/platform.rs b/crates/gpui2/src/platform.rs index 66cf7c14efb95ad520083122a900f9ff900c7e94..8a10173ca8dfb34d9a0f55d1344cbae92521a24f 100644 --- a/crates/gpui2/src/platform.rs +++ b/crates/gpui2/src/platform.rs @@ -145,6 +145,7 @@ pub trait PlatformWindow { fn mouse_position(&self) -> Point; fn as_any_mut(&mut self) -> &mut dyn Any; fn set_input_handler(&mut self, input_handler: Box); + fn clear_input_handler(&mut self); fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver; fn activate(&self); fn set_title(&mut self, title: &str); diff --git a/crates/gpui2/src/platform/mac/window.rs b/crates/gpui2/src/platform/mac/window.rs index ba9a67e1580c16dfdbfc0849d8d1678af308c96a..03ba635327fe3ae4201220315b172b7bbc2d6161 100644 --- a/crates/gpui2/src/platform/mac/window.rs +++ b/crates/gpui2/src/platform/mac/window.rs @@ -750,6 +750,10 @@ impl PlatformWindow for MacWindow { self.0.as_ref().lock().input_handler = Some(input_handler); } + fn clear_input_handler(&mut self) { + self.0.as_ref().lock().input_handler = None; + } + fn prompt(&self, level: PromptLevel, msg: &str, answers: &[&str]) -> oneshot::Receiver { // macOs applies overrides to modal window buttons after they are added. // Two most important for this logic are: diff --git a/crates/gpui2/src/platform/test/window.rs b/crates/gpui2/src/platform/test/window.rs index 2b9ee7798ea7c5cc6665e763228c2963f1520231..9b7ad7247206fcf5348f0351f1902e6135051366 100644 --- a/crates/gpui2/src/platform/test/window.rs +++ b/crates/gpui2/src/platform/test/window.rs @@ -89,6 +89,10 @@ impl PlatformWindow for TestWindow { self.input_handler = Some(Arc::new(Mutex::new(input_handler))); } + fn clear_input_handler(&mut self) { + self.input_handler = None; + } + fn prompt( &self, _level: crate::PromptLevel, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 6343f67988480622b45a3bbedfc0cfef99dfb04e..3a966871fc13091a5d77709ca3a0e4dc67282efd 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1231,6 +1231,7 @@ impl<'a> WindowContext<'a> { /// Rotate the current frame and the previous frame, then clear the current frame. /// We repopulate all state in the current frame during each paint. fn start_frame(&mut self) { + self.window.platform_window.clear_input_handler(); self.text_system().start_frame(); let window = &mut *self.window; diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 24d9527b4683268b1cc8c3ade78bcb850133f145..4d088ff63ba10124c17d94c7a04df93bb959d77a 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -1,11 +1,11 @@ use editor::{Cursor, HighlightedRange, HighlightedRangeLine}; use gpui::{ - black, div, point, px, red, relative, transparent_black, AnyElement, AvailableSpace, Bounds, - DispatchPhase, Element, ElementId, ElementInputHandler, FocusHandle, Font, FontStyle, + black, div, point, px, red, relative, transparent_black, AnyElement, AsyncWindowContext, + AvailableSpace, Bounds, DispatchPhase, Element, ElementId, FocusHandle, Font, FontStyle, FontWeight, HighlightStyle, Hsla, InteractiveElement, InteractiveElementState, IntoElement, - LayoutId, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, Point, Rgba, ShapedLine, - Size, StatefulInteractiveElement, Styled, TextRun, TextStyle, TextSystem, UnderlineStyle, View, - WeakModel, WhiteSpace, WindowContext, + LayoutId, Model, ModelContext, ModifiersChangedEvent, MouseButton, Pixels, + PlatformInputHandler, Point, Rgba, ShapedLine, Size, StatefulInteractiveElement, Styled, + TextRun, TextStyle, TextSystem, UnderlineStyle, View, WhiteSpace, WindowContext, }; use itertools::Itertools; use language::CursorShape; @@ -148,7 +148,7 @@ impl LayoutRect { ///The GPUI element that paints the terminal. ///We need to keep a reference to the view for mouse events, do we need it for any other terminal stuff, or can we move that to connection? pub struct TerminalElement { - terminal: WeakModel, + terminal: Model, terminal_view: View, focus: FocusHandle, focused: bool, @@ -167,7 +167,7 @@ impl StatefulInteractiveElement for TerminalElement {} impl TerminalElement { pub fn new( - terminal: WeakModel, + terminal: Model, terminal_view: View, focus: FocusHandle, focused: bool, @@ -461,16 +461,11 @@ impl TerminalElement { TerminalSize::new(line_height, cell_width, size) }; - let search_matches = if let Some(terminal_model) = self.terminal.upgrade() { - terminal_model.read(cx).matches.clone() - } else { - Default::default() - }; + let search_matches = self.terminal.read(cx).matches.clone(); let background_color = theme.colors().background; - let terminal_handle = self.terminal.upgrade().unwrap(); - let last_hovered_word = terminal_handle.update(cx, |terminal, cx| { + let last_hovered_word = self.terminal.update(cx, |terminal, cx| { terminal.set_size(dimensions); terminal.try_sync(cx); if self.can_navigate_to_selected_word && terminal.can_navigate_to_selected_word() { @@ -495,7 +490,7 @@ impl TerminalElement { selection, cursor, .. - } = &terminal_handle.read(cx).last_content; + } = &self.terminal.read(cx).last_content; // searches, highlights to a single range representations let mut relative_highlighted_ranges = Vec::new(); @@ -592,20 +587,18 @@ impl TerminalElement { } fn generic_button_handler( - connection: WeakModel, + connection: Model, origin: Point, focus_handle: FocusHandle, f: impl Fn(&mut Terminal, Point, &E, &mut ModelContext), ) -> impl Fn(&E, &mut WindowContext) { move |event, cx| { cx.focus(&focus_handle); - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - f(terminal, origin, event, cx); + connection.update(cx, |terminal, cx| { + f(terminal, origin, event, cx); - cx.notify(); - }) - } + cx.notify(); + }) } } @@ -617,10 +610,10 @@ impl TerminalElement { return; } - let handled = this - .update(cx, |term, _| term.try_modifiers_change(&event.modifiers)) - .ok(); - if handled == Some(true) { + let handled = + this.update(cx, |term, _| term.try_modifiers_change(&event.modifiers)); + + if handled { cx.notify(); } } @@ -645,13 +638,11 @@ impl TerminalElement { cx.focus(&focus); //todo!(context menu) // v.context_menu.update(cx, |menu, _cx| menu.delay_cancel()); - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_down(&e, origin); + connection.update(cx, |terminal, cx| { + terminal.mouse_down(&e, origin); - cx.notify(); - }) - } + cx.notify(); + }) } }) .on_mouse_move({ @@ -660,12 +651,10 @@ impl TerminalElement { move |e, cx| { if e.pressed_button.is_some() { if focus.is_focused(cx) { - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_drag(e, origin, bounds); - cx.notify(); - }) - } + connection.update(cx, |terminal, cx| { + terminal.mouse_drag(e, origin, bounds); + cx.notify(); + }) } } } @@ -685,14 +674,10 @@ impl TerminalElement { let connection = connection.clone(); move |e, cx| { if e.down.button == MouseButton::Right { - let mouse_mode = if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, _cx| { - terminal.mouse_mode(e.down.modifiers.shift) - }) - } else { - // If we can't get the model handle, probably can't deploy the context menu - true - }; + let mouse_mode = connection.update(cx, |terminal, _cx| { + terminal.mouse_mode(e.down.modifiers.shift) + }); + if !mouse_mode { //todo!(context menu) // view.deploy_context_menu(e.position, cx); @@ -705,24 +690,20 @@ impl TerminalElement { let focus = focus.clone(); move |e, cx| { if focus.is_focused(cx) { - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - terminal.mouse_move(&e, origin); - cx.notify(); - }) - } + connection.update(cx, |terminal, cx| { + terminal.mouse_move(&e, origin); + cx.notify(); + }) } } }) .on_scroll_wheel({ let connection = connection.clone(); move |e, cx| { - if let Some(conn_handle) = connection.upgrade() { - conn_handle.update(cx, |terminal, cx| { - terminal.scroll_wheel(e, origin); - cx.notify(); - }) - } + connection.update(cx, |terminal, cx| { + terminal.scroll_wheel(e, origin); + cx.notify(); + }) } }); @@ -822,13 +803,21 @@ impl Element for TerminalElement { ); let origin = bounds.origin + Point::new(layout.gutter, px(0.)); + let terminal_input_handler = TerminalInputHandler { + cx: cx.to_async(), + terminal: self.terminal.clone(), + cursor_bounds: layout + .cursor + .as_ref() + .map(|cursor| cursor.bounding_rect(origin)), + }; + let mut this = self.register_mouse_listeners(origin, layout.mode, bounds, cx); let interactivity = mem::take(&mut this.interactivity); interactivity.paint(bounds, bounds.size, state, cx, |_, _, cx| { - let input_handler = ElementInputHandler::new(bounds, this.terminal_view.clone(), cx); - cx.handle_input(&this.focus, input_handler); + cx.handle_input(&this.focus, terminal_input_handler); this.register_key_listeners(cx); @@ -890,6 +879,69 @@ impl IntoElement for TerminalElement { } } +struct TerminalInputHandler { + cx: AsyncWindowContext, + terminal: Model, + cursor_bounds: Option>, +} + +impl PlatformInputHandler for TerminalInputHandler { + fn selected_text_range(&mut self) -> Option> { + self.cx + .update(|_, cx| { + if self + .terminal + .read(cx) + .last_content + .mode + .contains(TermMode::ALT_SCREEN) + { + None + } else { + Some(0..0) + } + }) + .ok() + .flatten() + } + + fn marked_text_range(&mut self) -> Option> { + None + } + + fn text_for_range(&mut self, range_utf16: std::ops::Range) -> Option { + None + } + + fn replace_text_in_range( + &mut self, + _replacement_range: Option>, + text: &str, + ) { + self.cx + .update(|_, cx| { + self.terminal.update(cx, |terminal, _| { + terminal.input(text.into()); + }) + }) + .ok(); + } + + fn replace_and_mark_text_in_range( + &mut self, + _range_utf16: Option>, + _new_text: &str, + _new_selected_range: Option>, + ) { + } + + fn unmark_text(&mut self) {} + + fn bounds_for_range(&mut self, _range_utf16: std::ops::Range) -> Option> { + self.cursor_bounds + } +} + fn is_blank(cell: &IndexedCell) -> bool { if cell.c != ' ' { return false; diff --git a/crates/terminal_view2/src/terminal_view.rs b/crates/terminal_view2/src/terminal_view.rs index 1562ba38e7dd3ea79cf30f0bdeacc47405d9293d..75b019f2ef24639d12577854c59ceedb48607669 100644 --- a/crates/terminal_view2/src/terminal_view.rs +++ b/crates/terminal_view2/src/terminal_view.rs @@ -9,10 +9,9 @@ pub mod terminal_panel; // use crate::terminal_element::TerminalElement; use editor::{scroll::autoscroll::Autoscroll, Editor}; use gpui::{ - actions, div, point, px, size, Action, AnyElement, AppContext, Bounds, Div, EventEmitter, - FocusEvent, FocusHandle, Focusable, FocusableElement, FocusableView, Font, FontStyle, - FontWeight, InputHandler, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton, - MouseDownEvent, Pixels, Render, Task, View, VisualContext, WeakView, Subscription + actions, div, Action, AnyElement, AppContext, Div, EventEmitter, FocusEvent, FocusHandle, + Focusable, FocusableElement, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, + MouseButton, MouseDownEvent, Pixels, Render, Subscription, Task, View, VisualContext, WeakView, }; use language::Bias; use persistence::TERMINAL_DB; @@ -26,7 +25,6 @@ use terminal::{ Event, MaybeNavigationTarget, Terminal, }; use terminal_element::TerminalElement; -use theme::ThemeSettings; use ui::{h_stack, prelude::*, ContextMenu, Icon, IconElement, Label}; use util::{paths::PathLikeWithPosition, ResultExt}; use workspace::{ @@ -624,7 +622,7 @@ impl Render for TerminalView { type Element = Focusable
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - let terminal_handle = self.terminal.clone().downgrade(); + let terminal_handle = self.terminal.clone(); let this_view = cx.view().clone(); let self_id = cx.entity_id(); @@ -673,124 +671,6 @@ impl Render for TerminalView { } } -//todo!(Implement IME) -impl InputHandler for TerminalView { - fn text_for_range( - &mut self, - range: std::ops::Range, - cx: &mut ViewContext, - ) -> Option { - None - } - - fn selected_text_range( - &mut self, - cx: &mut ViewContext, - ) -> Option> { - if self - .terminal - .read(cx) - .last_content - .mode - .contains(TermMode::ALT_SCREEN) - { - None - } else { - Some(0..0) - } - } - - fn marked_text_range(&self, _cx: &mut ViewContext) -> Option> { - None - } - - fn unmark_text(&mut self, _cx: &mut ViewContext) {} - - fn replace_text_in_range( - &mut self, - _: Option>, - text: &str, - cx: &mut ViewContext, - ) { - self.terminal.update(cx, |terminal, _| { - terminal.input(text.into()); - }); - } - - fn replace_and_mark_text_in_range( - &mut self, - _range: Option>, - _new_text: &str, - _new_selected_range: Option>, - _cx: &mut ViewContext, - ) { - } - - // todo!(Check that this works correctly, why aren't we reading the range?) - fn bounds_for_range( - &mut self, - _range_utf16: std::ops::Range, - bounds: gpui::Bounds, - cx: &mut ViewContext, - ) -> Option> { - let settings = ThemeSettings::get_global(cx).clone(); - - let buffer_font_size = settings.buffer_font_size(cx); - - let terminal_settings = TerminalSettings::get_global(cx); - let font_family = terminal_settings - .font_family - .as_ref() - .map(|string| string.clone().into()) - .unwrap_or(settings.buffer_font.family); - - let line_height = terminal_settings - .line_height - .value() - .to_pixels(cx.rem_size()); - - let font_size = terminal_settings.font_size.clone(); - let features = terminal_settings - .font_features - .clone() - .unwrap_or(settings.buffer_font.features.clone()); - - let font_size = - font_size.map_or(buffer_font_size, |size| theme::adjusted_font_size(size, cx)); - - let font_id = cx - .text_system() - .font_id(&Font { - family: font_family, - style: FontStyle::Normal, - weight: FontWeight::NORMAL, - features, - }) - .unwrap(); - - let cell_width = cx - .text_system() - .advance(font_id, font_size, 'm') - .unwrap() - .width; - - let mut origin = bounds.origin + point(cell_width, px(0.)); - - // TODO - Why is it necessary to move downward one line to get correct - // positioning? I would think that we'd want the same rect that is - // painted for the cursor. - origin += point(px(0.), line_height); - - let cursor = Bounds { - origin, - //todo!(correctly calculate this width and height based on the text the line is over) - size: size(cell_width, line_height), - }; - - Some(cursor) - } -} - impl Item for TerminalView { type Event = ItemEvent; From c8cb1140b90bdcc9d75bd0191a23f4384a2c5a69 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Wed, 6 Dec 2023 19:10:15 -0500 Subject: [PATCH 88/90] Clean up how we open the recent projects picker (#3526) This PR performs some light cleanup of how we open the recent projects picker, to bring it more in-line with our other picker code. Release Notes: - N/A --- .../recent_projects2/src/recent_projects.rs | 76 +++++++++---------- 1 file changed, 35 insertions(+), 41 deletions(-) diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs index 03cd042f8207314e5eff53cf873d609e4f61605d..f6c2e0a2a2889daac6871ba2512bc297b203d93e 100644 --- a/crates/recent_projects2/src/recent_projects.rs +++ b/crates/recent_projects2/src/recent_projects.rs @@ -22,44 +22,6 @@ pub fn init(cx: &mut AppContext) { cx.observe_new_views(RecentProjects::register).detach(); } -fn toggle( - _: &mut Workspace, - _: &OpenRecent, - cx: &mut ViewContext, -) -> Option>> { - Some(cx.spawn(|workspace, mut cx| async move { - let workspace_locations: Vec<_> = cx - .background_executor() - .spawn(async { - WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .unwrap_or_default() - .into_iter() - .map(|(_, location)| location) - .collect() - }) - .await; - - workspace.update(&mut cx, |workspace, cx| { - if !workspace_locations.is_empty() { - let weak_workspace = cx.view().downgrade(); - workspace.toggle_modal(cx, |cx| { - let delegate = - RecentProjectsDelegate::new(weak_workspace, workspace_locations, true); - - RecentProjects::new(delegate, cx) - }); - } else { - workspace.show_notification(0, cx, |cx| { - cx.build_view(|_| MessageNotification::new("No recent projects to open.")) - }) - } - })?; - Ok(()) - })) -} - pub struct RecentProjects { picker: View>, } @@ -74,9 +36,7 @@ impl RecentProjects { fn register(workspace: &mut Workspace, _: &mut ViewContext) { workspace.register_action(|workspace, _: &OpenRecent, cx| { let Some(recent_projects) = workspace.active_modal::(cx) else { - // TODO(Marshall): Is this how we should be handling this? - // The previous code was using `cx.add_async_action` to invoke `toggle`. - if let Some(handler) = toggle(workspace, &OpenRecent, cx) { + if let Some(handler) = Self::open(workspace, cx) { handler.detach_and_log_err(cx); } return; @@ -89,6 +49,40 @@ impl RecentProjects { }); }); } + + fn open(_: &mut Workspace, cx: &mut ViewContext) -> Option>> { + Some(cx.spawn(|workspace, mut cx| async move { + let workspace_locations: Vec<_> = cx + .background_executor() + .spawn(async { + WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .map(|(_, location)| location) + .collect() + }) + .await; + + workspace.update(&mut cx, |workspace, cx| { + if !workspace_locations.is_empty() { + let weak_workspace = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = + RecentProjectsDelegate::new(weak_workspace, workspace_locations, true); + + RecentProjects::new(delegate, cx) + }); + } else { + workspace.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new("No recent projects to open.")) + }) + } + })?; + Ok(()) + })) + } } impl EventEmitter for RecentProjects {} From 22cd62213b550f5e2bf7102a67e8cad6b75c4011 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 6 Dec 2023 16:22:25 -0800 Subject: [PATCH 89/90] Tear out drag event listener --- crates/gpui2/src/app.rs | 30 +------------- crates/gpui2/src/elements/div.rs | 70 ++++++++------------------------ crates/gpui2/src/window.rs | 17 ++++---- 3 files changed, 26 insertions(+), 91 deletions(-) diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 800c86d569ff6f612b3ad9b49976d2858a89a26a..9293302938c13b8948fab22dd4fc092e69da6c80 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -188,7 +188,7 @@ pub struct AppContext { flushing_effects: bool, pending_updates: usize, pub(crate) actions: Rc, - pub(crate) active_drag: Option, + pub(crate) active_drag: Option, pub(crate) active_tooltip: Option, pub(crate) next_frame_callbacks: HashMap>, pub(crate) frame_consumers: HashMap>, @@ -1264,34 +1264,6 @@ pub struct AnyDrag { pub cursor_offset: Point, } -pub enum AnyDragState { - EventListener, - AnyDrag(AnyDrag), -} - -impl AnyDragState { - pub fn any_drag(&self) -> Option<&AnyDrag> { - match self { - AnyDragState::EventListener => None, - AnyDragState::AnyDrag(any_drag) => Some(any_drag), - } - } - - pub fn entity_id(&self) -> Option { - match self { - AnyDragState::EventListener => None, - AnyDragState::AnyDrag(any_drag) => Some(any_drag.view.entity_id()), - } - } - - pub fn entity_type(&self) -> Option { - match self { - AnyDragState::EventListener => None, - AnyDragState::AnyDrag(any_drag) => Some(any_drag.view.entity_type()), - } - } -} - #[derive(Clone)] pub(crate) struct AnyTooltip { pub view: AnyView, diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index ab2124314308e8e2572cbdc6c612c1cbf94c8c02..10fd7dda0a4d710c1751333dcbf197793761f088 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1,10 +1,10 @@ use crate::{ - point, px, Action, AnyDrag, AnyDragState, AnyElement, AnyTooltip, AnyView, AppContext, - BorrowAppContext, BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, - FocusEvent, FocusHandle, IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, - MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, - Render, ScrollWheelEvent, SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, - Task, View, Visibility, WindowContext, + point, px, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, AppContext, BorrowAppContext, + BorrowWindow, Bounds, ClickEvent, DispatchPhase, Element, ElementId, FocusEvent, FocusHandle, + IntoElement, KeyContext, KeyDownEvent, KeyUpEvent, LayoutId, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Render, ScrollWheelEvent, + SharedString, Size, StackingOrder, Style, StyleRefinement, Styled, Task, View, Visibility, + WindowContext, }; use collections::HashMap; use refineable::Refineable; @@ -415,19 +415,6 @@ pub trait StatefulInteractiveElement: InteractiveElement { self } - fn on_drag_event( - mut self, - listener: impl Fn(&MouseMoveEvent, &mut WindowContext) + 'static, - ) -> Self - where - Self: Sized, - { - self.interactivity() - .drag_event_listeners - .push(Box::new(listener)); - self - } - fn on_hover(mut self, listener: impl Fn(&bool, &mut WindowContext) + 'static) -> Self where Self: Sized, @@ -761,7 +748,6 @@ pub struct Interactivity { pub action_listeners: SmallVec<[(TypeId, ActionListener); 8]>, pub drop_listeners: SmallVec<[(TypeId, Box); 2]>, pub click_listeners: SmallVec<[ClickListener; 2]>, - pub drag_event_listeners: SmallVec<[DragEventListener; 1]>, pub drag_listener: Option, pub hover_listener: Option>, pub tooltip_builder: Option, @@ -906,11 +892,8 @@ impl Interactivity { if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, &cx) { - if let Some(drag_state_type) = cx - .active_drag - .as_ref() - .and_then(|drag| drag.any_drag()) - .map(|drag| drag.view.entity_type()) + if let Some(drag_state_type) = + cx.active_drag.as_ref().map(|drag| drag.view.entity_type()) { for (drop_state_type, listener) in &drop_listeners { if *drop_state_type == drag_state_type { @@ -918,7 +901,7 @@ impl Interactivity { .active_drag .take() .expect("checked for type drag state type above"); - let drag = drag.any_drag().expect("checked for any drag above"); + listener(drag.view.clone(), cx); cx.notify(); cx.stop_propagation(); @@ -933,16 +916,12 @@ impl Interactivity { let click_listeners = mem::take(&mut self.click_listeners); let drag_listener = mem::take(&mut self.drag_listener); - let drag_event_listeners = mem::take(&mut self.drag_event_listeners); - if !click_listeners.is_empty() - || drag_listener.is_some() - || !drag_event_listeners.is_empty() - { + if !click_listeners.is_empty() || drag_listener.is_some() { let pending_mouse_down = element_state.pending_mouse_down.clone(); let mouse_down = pending_mouse_down.borrow().clone(); if let Some(mouse_down) = mouse_down { - if !drag_event_listeners.is_empty() || drag_listener.is_some() { + if let Some(drag_listener) = drag_listener { let active_state = element_state.clicked_state.clone(); let interactive_bounds = interactive_bounds.clone(); @@ -950,29 +929,17 @@ impl Interactivity { if cx.active_drag.is_some() { if phase == DispatchPhase::Capture { cx.notify(); - } else if interactive_bounds.visibly_contains(&event.position, cx) - && (event.position - mouse_down.position).magnitude() - > DRAG_THRESHOLD - { - for listener in &drag_event_listeners { - listener(event, cx); - } } } else if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, cx) && (event.position - mouse_down.position).magnitude() > DRAG_THRESHOLD { *active_state.borrow_mut() = ElementClickedState::default(); - if let Some(drag_listener) = &drag_listener { - let cursor_offset = event.position - bounds.origin; - let drag = drag_listener(cursor_offset, cx); - cx.active_drag = Some(AnyDragState::AnyDrag(drag)); - cx.notify(); - cx.stop_propagation(); - } - for listener in &drag_event_listeners { - listener(event, cx); - } + let cursor_offset = event.position - bounds.origin; + let drag = drag_listener(cursor_offset, cx); + cx.active_drag = Some(drag); + cx.notify(); + cx.stop_propagation(); } }); } @@ -1235,7 +1202,7 @@ impl Interactivity { if let Some(drag) = cx.active_drag.take() { for (state_type, group_drag_style) in &self.group_drag_over_styles { if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) { - if Some(*state_type) == drag.entity_type() + if *state_type == drag.view.entity_type() && group_bounds.contains_point(&mouse_position) { style.refine(&group_drag_style.style); @@ -1244,7 +1211,7 @@ impl Interactivity { } for (state_type, drag_over_style) in &self.drag_over_styles { - if Some(*state_type) == drag.entity_type() + if *state_type == drag.view.entity_type() && bounds .intersect(&cx.content_mask().bounds) .contains_point(&mouse_position) @@ -1301,7 +1268,6 @@ impl Default for Interactivity { action_listeners: SmallVec::new(), drop_listeners: SmallVec::new(), click_listeners: SmallVec::new(), - drag_event_listeners: SmallVec::new(), drag_listener: None, hover_listener: None, tooltip_builder: None, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 3a966871fc13091a5d77709ca3a0e4dc67282efd..455472a349449dfa5e84d7a285a789a3402d35f6 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1188,14 +1188,11 @@ impl<'a> WindowContext<'a> { }); if let Some(active_drag) = self.app.active_drag.take() { - if let Some(active_drag) = active_drag.any_drag() { - self.with_z_index(1, |cx| { - let offset = cx.mouse_position() - active_drag.cursor_offset; - let available_space = - size(AvailableSpace::MinContent, AvailableSpace::MinContent); - active_drag.view.draw(offset, available_space, cx); - }); - } + self.with_z_index(1, |cx| { + let offset = cx.mouse_position() - active_drag.cursor_offset; + let available_space = size(AvailableSpace::MinContent, AvailableSpace::MinContent); + active_drag.view.draw(offset, available_space, cx); + }); self.active_drag = Some(active_drag); } else if let Some(active_tooltip) = self.app.active_tooltip.take() { self.with_z_index(1, |cx| { @@ -1273,10 +1270,10 @@ impl<'a> WindowContext<'a> { FileDropEvent::Entered { position, files } => { self.window.mouse_position = position; if self.active_drag.is_none() { - self.active_drag = Some(crate::AnyDragState::AnyDrag(AnyDrag { + self.active_drag = Some(AnyDrag { view: self.build_view(|_| files).into(), cursor_offset: position, - })); + }); } InputEvent::MouseDown(MouseDownEvent { position, From 66b93212fe9257d49769df25a581ae50d25d7a63 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Wed, 6 Dec 2023 16:25:50 -0800 Subject: [PATCH 90/90] Fix selection backgrounds --- crates/terminal_view2/src/terminal_element.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 4d088ff63ba10124c17d94c7a04df93bb959d77a..4a37ca91b779e6aa9c09fe510a1f5af9bbe6c48b 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -261,7 +261,6 @@ impl TerminalElement { let cell_style = TerminalElement::cell_style( &cell, fg, - bg, theme, text_style, text_system, @@ -274,7 +273,6 @@ impl TerminalElement { text_style.font_size.to_pixels(cx.rem_size()), &[cell_style], ) - //todo!() Can we remove this unwrap? .unwrap(); cells.push(LayoutCell::new( @@ -324,7 +322,7 @@ impl TerminalElement { fn cell_style( indexed: &IndexedCell, fg: terminal::alacritty_terminal::ansi::Color, - bg: terminal::alacritty_terminal::ansi::Color, + // bg: terminal::alacritty_terminal::ansi::Color, colors: &Theme, text_style: &TextStyle, text_system: &TextSystem, @@ -332,7 +330,7 @@ impl TerminalElement { ) -> TextRun { let flags = indexed.cell.flags; let fg = convert_color(&fg, &colors); - let bg = convert_color(&bg, &colors); + // let bg = convert_color(&bg, &colors); let underline = (flags.intersects(Flags::ALL_UNDERLINES) || indexed.cell.hyperlink().is_some()) @@ -357,7 +355,7 @@ impl TerminalElement { let mut result = TextRun { len: indexed.c.len_utf8() as usize, color: fg, - background_color: Some(bg), + background_color: None, font: Font { weight, style, @@ -384,10 +382,6 @@ impl TerminalElement { fn compute_layout(&self, bounds: Bounds, cx: &mut WindowContext) -> LayoutState { let settings = ThemeSettings::get_global(cx).clone(); - //Setup layout information - // todo!(Terminal tooltips) - // let tooltip_style = settings.theme.tooltip.clone(); - let buffer_font_size = settings.buffer_font_size(cx); let terminal_settings = TerminalSettings::get_global(cx);