From 2678dfdc57208737bf712793ee489b770aae440d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 29 Jun 2023 17:32:04 -0400 Subject: [PATCH 01/29] Update assistant styles --- styles/src/style_tree/assistant.ts | 319 +++++++++++------------------ 1 file changed, 120 insertions(+), 199 deletions(-) diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index bdde221acae12f47711f46ea66c5dcc1fbb21d31..1b754b632ce2c78c2a48869d486ed243f7a8b3a7 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -1,232 +1,164 @@ -import { ColorScheme } from "../theme/color_scheme" -import { text, border, background, foreground } from "./components" -import { interactive } from "../element" +import { ColorScheme, StyleSets } from "../theme/color_scheme" +import { text, border, background, foreground, TextStyle } from "./components" +import { Interactive, interactive } from "../element" + +interface ToolbarButtonOptions { + icon: string +} + +type RoleCycleButton = TextStyle & { + background?: string +} +// TODO: Replace these with zed types +type RemainingTokens = TextStyle & { + background: string, + margin: { top: number, right: number }, + padding: { + right: number, + left: number, + top: number, + bottom: number, + }, + corner_radius: number, +} export default function assistant(theme: ColorScheme): any { + const TOOLBAR_SPACING = 8 + + const toolbar_button = ({ icon }: ToolbarButtonOptions) => { + return ( + interactive({ + base: { + icon: { + color: foreground(theme.highest, "variant"), + asset: icon, + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + padding: { left: TOOLBAR_SPACING, right: TOOLBAR_SPACING }, + }, + }, + state: { + hovered: { + icon: { + color: foreground(theme.highest, "hovered"), + }, + }, + }, + }) + ) + } + + const interactive_role = (color: StyleSets): Interactive => { + return ( + interactive({ + base: { + ...text(theme.highest, "sans", color, { size: "sm" }), + }, + state: { + hovered: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "hovered"), + }, + clicked: { + ...text(theme.highest, "sans", color, { size: "sm" }), + background: background(theme.highest, color, "pressed"), + } + }, + }) + ) + } + + const tokens_remaining = (color: StyleSets): RemainingTokens => { + return ( + { + ...text(theme.highest, "mono", color, { size: "xs" }), + background: background(theme.highest, "on", "default"), + margin: { top: 12, right: 8 }, + padding: { right: 4, left: 4, top: 1, bottom: 1 }, + corner_radius: 6, + } + ) + } + return { container: { background: background(theme.highest), padding: { left: 12 }, }, message_header: { - margin: { bottom: 6, top: 6 }, + margin: { bottom: 4, top: 4 }, background: background(theme.highest), }, - hamburger_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/hamburger_15.svg", - dimensions: { - width: 15, - height: 15, - }, - }, - container: { - padding: { left: 12, right: 8.5 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + hamburger_button: toolbar_button({ + icon: "icons/hamburger_15.svg", }), - split_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/split_message_15.svg", - dimensions: { - width: 15, - height: 15, - }, - }, - container: { - padding: { left: 8.5, right: 8.5 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + + split_button: toolbar_button({ + icon: "icons/split_message_15.svg", }), - quote_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/quote_15.svg", - dimensions: { - width: 15, - height: 15, - }, - }, - container: { - padding: { left: 8.5, right: 8.5 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + quote_button: toolbar_button({ + icon: "icons/radix/quote.svg", }), - assist_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/assist_15.svg", - dimensions: { - width: 15, - height: 15, - }, - }, - container: { - padding: { left: 8.5, right: 8.5 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + assist_button: toolbar_button({ + icon: "icons/radix/magic-wand.svg", }), - zoom_in_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/maximize_8.svg", - dimensions: { - width: 12, - height: 12, - }, - }, - container: { - padding: { left: 10, right: 10 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + zoom_in_button: toolbar_button({ + icon: "icons/radix/enter-full-screen.svg", }), - zoom_out_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/minimize_8.svg", - dimensions: { - width: 12, - height: 12, - }, - }, - container: { - padding: { left: 10, right: 10 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + zoom_out_button: toolbar_button({ + icon: "icons/radix/exit-full-screen.svg", }), - plus_button: interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: "icons/plus_12.svg", - dimensions: { - width: 12, - height: 12, - }, - }, - container: { - padding: { left: 10, right: 10 }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, + plus_button: toolbar_button({ + icon: "icons/radix/plus.svg", }), title: { - ...text(theme.highest, "sans", "default", { size: "sm" }), + ...text(theme.highest, "sans", "default", { size: "xs" }), }, saved_conversation: { container: interactive({ base: { - background: background(theme.highest, "on"), + background: background(theme.middle), padding: { top: 4, bottom: 4 }, + border: border(theme.middle, "default", { top: true, overlay: true }), }, state: { hovered: { - background: background(theme.highest, "on", "hovered"), + background: background(theme.middle, "hovered"), }, + clicked: { + background: background(theme.middle, "pressed"), + } }, }), saved_at: { margin: { left: 8 }, - ...text(theme.highest, "sans", "default", { size: "xs" }), + ...text(theme.highest, "sans", "variant", { size: "xs" }), }, title: { - margin: { left: 16 }, + margin: { left: 12 }, ...text(theme.highest, "sans", "default", { size: "sm", weight: "bold", }), }, }, - user_sender: { - default: { - ...text(theme.highest, "sans", "default", { - size: "sm", - weight: "bold", - }), - }, - }, - assistant_sender: { - default: { - ...text(theme.highest, "sans", "accent", { - size: "sm", - weight: "bold", - }), - }, - }, - system_sender: { - default: { - ...text(theme.highest, "sans", "variant", { - size: "sm", - weight: "bold", - }), - }, - }, + user_sender: interactive_role("base"), + assistant_sender: interactive_role("accent"), + system_sender: interactive_role("warning"), sent_at: { margin: { top: 2, left: 8 }, - ...text(theme.highest, "sans", "default", { size: "2xs" }), + ...text(theme.highest, "sans", "variant", { size: "2xs" }), }, model: interactive({ base: { - background: background(theme.highest, "on"), - margin: { left: 12, right: 12, top: 12 }, - padding: 4, - corner_radius: 4, + background: background(theme.highest), + margin: { left: 12, right: 4, top: 12 }, + padding: { right: 4, left: 4, top: 1, bottom: 1 }, + corner_radius: 6, ...text(theme.highest, "sans", "default", { size: "xs" }), }, state: { @@ -236,20 +168,9 @@ export default function assistant(theme: ColorScheme): any { }, }, }), - remaining_tokens: { - background: background(theme.highest, "on"), - margin: { top: 12, right: 24 }, - padding: 4, - corner_radius: 4, - ...text(theme.highest, "sans", "positive", { size: "xs" }), - }, - no_remaining_tokens: { - background: background(theme.highest, "on"), - margin: { top: 12, right: 24 }, - padding: 4, - corner_radius: 4, - ...text(theme.highest, "sans", "negative", { size: "xs" }), - }, + remaining_tokens: tokens_remaining("positive"), + low_remaining_tokens: tokens_remaining("warning"), + no_remaining_tokens: tokens_remaining("negative"), error_icon: { margin: { left: 8 }, color: foreground(theme.highest, "negative"), From d6112e4a594ad64578d663547032ab3161d4304d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 29 Jun 2023 17:32:19 -0400 Subject: [PATCH 02/29] Add doc comments for ColorScheme layer properties --- styles/src/theme/color_scheme.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/styles/src/theme/color_scheme.ts b/styles/src/theme/color_scheme.ts index 933c6160535386dea3f7b3b517c3d95b234cbcbe..c64be951841dac22b5d3b984ee0abea5876a584b 100644 --- a/styles/src/theme/color_scheme.ts +++ b/styles/src/theme/color_scheme.ts @@ -12,8 +12,17 @@ export interface ColorScheme { name: string is_light: boolean + /** + * App background, other elements that should sit directly on top of the background. + */ lowest: Layer + /** + * Panels, tabs, other UI surfaces that sit on top of the background. + */ middle: Layer + /** + * Editors like code buffers, conversation editors, etc. + */ highest: Layer ramps: RampSet From 77b120323b9deb05d40904aaab26ccfc6342409f Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 29 Jun 2023 17:44:47 -0400 Subject: [PATCH 03/29] Add `low_tokens_remaining` case to the assistant --- crates/ai/src/assistant.rs | 2 ++ crates/theme/src/theme.rs | 1 + 2 files changed, 3 insertions(+) diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 9ca54e661a74d935356bf52378613e9ffb1dcb0e..6375c2fe4d089ae8db44c9138a33fe845334fe38 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -2060,6 +2060,8 @@ impl ConversationEditor { 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 }; diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index e54dcdfd1e987eaf24656bc735079db54d37f0bc..01da555e1edf1d33486c0684270dc325ec97f20f 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -1027,6 +1027,7 @@ pub struct AssistantStyle { pub system_sender: Interactive, pub model: Interactive, pub remaining_tokens: ContainedText, + pub low_remaining_tokens: ContainedText, pub no_remaining_tokens: ContainedText, pub error_icon: Icon, pub api_key_editor: FieldEditor, From 530561e4ebb6db420f5759cc23ab48f5bf33a29b Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 29 Jun 2023 18:13:31 -0400 Subject: [PATCH 04/29] Extract assistant tool buttons into `tab_bar_button` --- styles/src/component/tab_bar_button.ts | 55 ++++++++++++++++++++++++++ styles/src/style_tree/assistant.ts | 47 +++++----------------- 2 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 styles/src/component/tab_bar_button.ts diff --git a/styles/src/component/tab_bar_button.ts b/styles/src/component/tab_bar_button.ts new file mode 100644 index 0000000000000000000000000000000000000000..2b657e8b37ff2a4860a51e05dc003c2d3f77368c --- /dev/null +++ b/styles/src/component/tab_bar_button.ts @@ -0,0 +1,55 @@ +import { ColorScheme, StyleSets } from "../common" +import { interactive } from "../element" +import { InteractiveState } from "../element/interactive" +import { background, foreground } from "../style_tree/components" + +interface TabBarButtonOptions { + icon: string + color?: StyleSets +} + +type TabBarButtonProps = TabBarButtonOptions & { + state?: Partial>> +} + +export function tab_bar_button(theme: ColorScheme, { icon, color = "base" }: TabBarButtonProps) { + const button_spacing = 8 + + return ( + interactive({ + base: { + icon: { + color: foreground(theme.middle, color), + asset: icon, + dimensions: { + width: 15, + height: 15, + }, + }, + container: { + corner_radius: 4, + padding: { + top: 4, bottom: 4, left: 4, right: 4 + }, + margin: { + left: button_spacing / 2, + right: button_spacing / 2, + }, + }, + }, + state: { + hovered: { + container: { + background: background(theme.middle, color, "hovered"), + + } + }, + clicked: { + container: { + background: background(theme.middle, color, "pressed"), + } + }, + }, + }) + ) +} diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 1b754b632ce2c78c2a48869d486ed243f7a8b3a7..802e4139cb60aaac079a00550fb7713d0abf7a8a 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -1,6 +1,7 @@ import { ColorScheme, StyleSets } from "../theme/color_scheme" import { text, border, background, foreground, TextStyle } from "./components" import { Interactive, interactive } from "../element" +import { tab_bar_button } from "../component/tab_bar_button" interface ToolbarButtonOptions { icon: string @@ -23,34 +24,6 @@ type RemainingTokens = TextStyle & { } export default function assistant(theme: ColorScheme): any { - const TOOLBAR_SPACING = 8 - - const toolbar_button = ({ icon }: ToolbarButtonOptions) => { - return ( - interactive({ - base: { - icon: { - color: foreground(theme.highest, "variant"), - asset: icon, - dimensions: { - width: 15, - height: 15, - }, - }, - container: { - padding: { left: TOOLBAR_SPACING, right: TOOLBAR_SPACING }, - }, - }, - state: { - hovered: { - icon: { - color: foreground(theme.highest, "hovered"), - }, - }, - }, - }) - ) - } const interactive_role = (color: StyleSets): Interactive => { return ( @@ -93,26 +66,26 @@ export default function assistant(theme: ColorScheme): any { margin: { bottom: 4, top: 4 }, background: background(theme.highest), }, - hamburger_button: toolbar_button({ + hamburger_button: tab_bar_button(theme, { icon: "icons/hamburger_15.svg", }), - split_button: toolbar_button({ + split_button: tab_bar_button(theme, { icon: "icons/split_message_15.svg", }), - quote_button: toolbar_button({ + quote_button: tab_bar_button(theme, { icon: "icons/radix/quote.svg", }), - assist_button: toolbar_button({ + assist_button: tab_bar_button(theme, { icon: "icons/radix/magic-wand.svg", }), - zoom_in_button: toolbar_button({ + zoom_in_button: tab_bar_button(theme, { icon: "icons/radix/enter-full-screen.svg", }), - zoom_out_button: toolbar_button({ + zoom_out_button: tab_bar_button(theme, { icon: "icons/radix/exit-full-screen.svg", }), - plus_button: toolbar_button({ + plus_button: tab_bar_button(theme, { icon: "icons/radix/plus.svg", }), title: { @@ -158,7 +131,7 @@ export default function assistant(theme: ColorScheme): any { background: background(theme.highest), margin: { left: 12, right: 4, top: 12 }, padding: { right: 4, left: 4, top: 1, bottom: 1 }, - corner_radius: 6, + corner_radius: 4, ...text(theme.highest, "sans", "default", { size: "xs" }), }, state: { @@ -178,7 +151,7 @@ export default function assistant(theme: ColorScheme): any { }, api_key_editor: { background: background(theme.highest, "on"), - corner_radius: 6, + corner_radius: 4, text: text(theme.highest, "mono", "on"), placeholder_text: text(theme.highest, "mono", "on", "disabled", { size: "xs", From 9ee2707d43c877e8f2f9f7efa7b0d26e90d48833 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 29 Jun 2023 22:45:54 -0600 Subject: [PATCH 05/29] vim: Add }/{ for start/end of paragraph Fixes: zed-industries/community#470 --- assets/keymaps/vim.json | 2 + crates/editor/src/editor.rs | 14 ++-- crates/editor/src/movement.rs | 24 +++++-- crates/vim/src/motion.rs | 118 +++++++++++++++++++++++++++++++++- 4 files changed, 149 insertions(+), 9 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index afee6fcd2e998500ba55e08230473e91e68dbb96..e4c489c5b5e5b328f682c9e68e29ace596389684 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -37,6 +37,8 @@ "$": "vim::EndOfLine", "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", + "{": "vim::StartOfParagraph", + "}": "vim::EndOfParagraph", "shift-w": [ "vim::NextWordStart", { diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 64332c102aa8a802bb6e428250b820597060590c..824802630dd88d194be61570d7bfeca4f2f087d6 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -5120,7 +5120,7 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { selection.collapse_to( - movement::start_of_paragraph(map, selection.head()), + movement::start_of_paragraph(map, selection.head(), 1), SelectionGoal::None, ) }); @@ -5140,7 +5140,7 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_with(|map, selection| { selection.collapse_to( - movement::end_of_paragraph(map, selection.head()), + movement::end_of_paragraph(map, selection.head(), 1), SelectionGoal::None, ) }); @@ -5159,7 +5159,10 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, _| { - (movement::start_of_paragraph(map, head), SelectionGoal::None) + ( + movement::start_of_paragraph(map, head, 1), + SelectionGoal::None, + ) }); }) } @@ -5176,7 +5179,10 @@ impl Editor { self.change_selections(Some(Autoscroll::fit()), cx, |s| { s.move_heads_with(|map, head, _| { - (movement::end_of_paragraph(map, head), SelectionGoal::None) + ( + movement::end_of_paragraph(map, head, 1), + SelectionGoal::None, + ) }); }) } diff --git a/crates/editor/src/movement.rs b/crates/editor/src/movement.rs index 523a0af9640aa98b3f3e3d7b9fd980768f1e4f89..8f1e6172e9ec28043b3d684ff5d8da3b3e4406e4 100644 --- a/crates/editor/src/movement.rs +++ b/crates/editor/src/movement.rs @@ -193,7 +193,11 @@ pub fn next_subword_end(map: &DisplaySnapshot, point: DisplayPoint) -> DisplayPo }) } -pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { +pub fn start_of_paragraph( + map: &DisplaySnapshot, + display_point: DisplayPoint, + mut count: usize, +) -> DisplayPoint { let point = display_point.to_point(map); if point.row == 0 { return map.max_point(); @@ -203,7 +207,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> for row in (0..point.row + 1).rev() { let blank = map.buffer_snapshot.is_line_blank(row); if found_non_blank_line && blank { - return Point::new(row, 0).to_display_point(map); + if count <= 1 { + return Point::new(row, 0).to_display_point(map); + } + count -= 1; + found_non_blank_line = false; } found_non_blank_line |= !blank; @@ -212,7 +220,11 @@ pub fn start_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint::zero() } -pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint { +pub fn end_of_paragraph( + map: &DisplaySnapshot, + display_point: DisplayPoint, + mut count: usize, +) -> DisplayPoint { let point = display_point.to_point(map); if point.row == map.max_buffer_row() { return DisplayPoint::zero(); @@ -222,7 +234,11 @@ pub fn end_of_paragraph(map: &DisplaySnapshot, display_point: DisplayPoint) -> D for row in point.row..map.max_buffer_row() + 1 { let blank = map.buffer_snapshot.is_line_blank(row); if found_non_blank_line && blank { - return Point::new(row, 0).to_display_point(map); + if count <= 1 { + return Point::new(row, 0).to_display_point(map); + } + count -= 1; + found_non_blank_line = false; } found_non_blank_line |= !blank; diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index faf69d94734f95cd79dabaef3a165bbec7721c81..f39cd82fc144cc70c84828648f10aad977228259 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -31,6 +31,8 @@ pub enum Motion { CurrentLine, StartOfLine, EndOfLine, + StartOfParagraph, + EndOfParagraph, StartOfDocument, EndOfDocument, Matching, @@ -72,6 +74,8 @@ actions!( StartOfLine, EndOfLine, CurrentLine, + StartOfParagraph, + EndOfParagraph, StartOfDocument, EndOfDocument, Matching, @@ -92,6 +96,12 @@ pub fn init(cx: &mut AppContext) { cx.add_action(|_: &mut Workspace, _: &StartOfLine, cx: _| motion(Motion::StartOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &EndOfLine, cx: _| motion(Motion::EndOfLine, cx)); cx.add_action(|_: &mut Workspace, _: &CurrentLine, cx: _| motion(Motion::CurrentLine, cx)); + cx.add_action(|_: &mut Workspace, _: &StartOfParagraph, cx: _| { + motion(Motion::StartOfParagraph, cx) + }); + cx.add_action(|_: &mut Workspace, _: &EndOfParagraph, cx: _| { + motion(Motion::EndOfParagraph, cx) + }); cx.add_action(|_: &mut Workspace, _: &StartOfDocument, cx: _| { motion(Motion::StartOfDocument, cx) }); @@ -142,7 +152,8 @@ impl Motion { pub fn linewise(&self) -> bool { use Motion::*; match self { - Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart => true, + Down | Up | StartOfDocument | EndOfDocument | CurrentLine | NextLineStart + | StartOfParagraph | EndOfParagraph => true, EndOfLine | NextWordEnd { .. } | Matching @@ -172,6 +183,8 @@ impl Motion { | Backspace | Right | StartOfLine + | StartOfParagraph + | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace @@ -197,6 +210,8 @@ impl Motion { | Backspace | Right | StartOfLine + | StartOfParagraph + | EndOfParagraph | NextWordStart { .. } | PreviousWordStart { .. } | FirstNonWhitespace @@ -235,6 +250,14 @@ impl Motion { FirstNonWhitespace => (first_non_whitespace(map, point), SelectionGoal::None), StartOfLine => (start_of_line(map, point), SelectionGoal::None), EndOfLine => (end_of_line(map, point), SelectionGoal::None), + StartOfParagraph => ( + movement::start_of_paragraph(map, point, times), + SelectionGoal::None, + ), + EndOfParagraph => ( + movement::end_of_paragraph(map, point, times), + SelectionGoal::None, + ), CurrentLine => (end_of_line(map, point), SelectionGoal::None), StartOfDocument => (start_of_document(map, point, times), SelectionGoal::None), EndOfDocument => ( @@ -590,3 +613,96 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> let new_row = (point.row() + times as u32).min(map.max_buffer_row()); map.clip_point(DisplayPoint::new(new_row, 0), Bias::Left) } + +#[cfg(test)] + +mod test { + + use crate::{state::Mode, test::VimTestContext}; + use indoc::indoc; + + #[gpui::test] + async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + let initial_state = indoc! {r"ˇabc + def + + paragraph + the second + + + + third and + final"}; + + // goes down once + cx.set_state(initial_state, Mode::Normal); + cx.simulate_keystrokes(["}"]); + cx.assert_state( + indoc! {r"abc + def + ˇ + paragraph + the second + + + + third and + final"}, + Mode::Normal, + ); + + // goes up once + cx.simulate_keystrokes(["{"]); + cx.assert_state(initial_state, Mode::Normal); + + // goes down twice + cx.simulate_keystrokes(["2", "}"]); + cx.assert_state( + indoc! {r"abc + def + + paragraph + the second + ˇ + + + third and + final"}, + Mode::Normal, + ); + + // goes down over multiple blanks + cx.simulate_keystrokes(["}"]); + cx.assert_state( + indoc! {r"abc + def + + paragraph + the second + + + + third and + finalˇ"}, + Mode::Normal, + ); + + // goes up twice + cx.simulate_keystrokes(["2", "{"]); + cx.assert_state( + indoc! {r"abc + def + ˇ + paragraph + the second + + + + third and + final"}, + Mode::Normal, + ) + } +} From abb58c41dbc8b31c02873ba294daf7530ff51e5e Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Thu, 29 Jun 2023 23:24:51 -0600 Subject: [PATCH 06/29] vim: Fix edge-case in } when trailing newline is absent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added .assert_shared_state() to NeovimBackedTestContext – although it's not strictly necessary to show the expected behaviour in the test file (as we can just compare to neovim's JSON recording), it makes it much easier to understand what we're testing. --- crates/editor/src/test/editor_test_context.rs | 26 ++++++---- crates/vim/src/motion.rs | 52 ++++++++----------- .../src/test/neovim_backed_test_context.rs | 51 ++++++++++++++---- .../test_start_end_of_paragraph.json | 13 +++++ 4 files changed, 94 insertions(+), 48 deletions(-) create mode 100644 crates/vim/test_data/test_start_end_of_paragraph.json diff --git a/crates/editor/src/test/editor_test_context.rs b/crates/editor/src/test/editor_test_context.rs index 95da7ff297341e18f99ca57f7cbcadb707a8fdb4..bac70f139a6be8732e9b2b8bde9bce2b63144418 100644 --- a/crates/editor/src/test/editor_test_context.rs +++ b/crates/editor/src/test/editor_test_context.rs @@ -210,6 +210,10 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, marked_text.to_string()) } + pub fn editor_state(&mut self) -> String { + generate_marked_text(self.buffer_text().as_str(), &self.editor_selections(), true) + } + #[track_caller] pub fn assert_editor_background_highlights(&mut self, marked_text: &str) { let expected_ranges = self.ranges(marked_text); @@ -248,14 +252,8 @@ impl<'a> EditorTestContext<'a> { self.assert_selections(expected_selections, expected_marked_text) } - #[track_caller] - fn assert_selections( - &mut self, - expected_selections: Vec>, - expected_marked_text: String, - ) { - let actual_selections = self - .editor + fn editor_selections(&self) -> Vec> { + self.editor .read_with(self.cx, |editor, cx| editor.selections.all::(cx)) .into_iter() .map(|s| { @@ -265,12 +263,22 @@ impl<'a> EditorTestContext<'a> { s.start..s.end } }) - .collect::>(); + .collect::>() + } + + #[track_caller] + fn assert_selections( + &mut self, + expected_selections: Vec>, + expected_marked_text: String, + ) { + let actual_selections = self.editor_selections(); let actual_marked_text = generate_marked_text(&self.buffer_text(), &actual_selections, true); if expected_selections != actual_selections { panic!( indoc! {" + {}Editor has unexpected selections. Expected selections: diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index f39cd82fc144cc70c84828648f10aad977228259..e8084cb4be13f8ffd3214b50bba1cc071d32193b 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -255,7 +255,7 @@ impl Motion { SelectionGoal::None, ), EndOfParagraph => ( - movement::end_of_paragraph(map, point, times), + map.clip_at_line_end(movement::end_of_paragraph(map, point, times)), SelectionGoal::None, ), CurrentLine => (end_of_line(map, point), SelectionGoal::None), @@ -618,12 +618,12 @@ fn next_line_start(map: &DisplaySnapshot, point: DisplayPoint, times: usize) -> mod test { - use crate::{state::Mode, test::VimTestContext}; + use crate::test::NeovimBackedTestContext; use indoc::indoc; #[gpui::test] async fn test_start_end_of_paragraph(cx: &mut gpui::TestAppContext) { - let mut cx = VimTestContext::new(cx, true).await; + let mut cx = NeovimBackedTestContext::new(cx).await; let initial_state = indoc! {r"ˇabc def @@ -637,10 +637,9 @@ mod test { final"}; // goes down once - cx.set_state(initial_state, Mode::Normal); - cx.simulate_keystrokes(["}"]); - cx.assert_state( - indoc! {r"abc + cx.set_shared_state(initial_state).await; + cx.simulate_shared_keystrokes(["}"]).await; + cx.assert_shared_state(indoc! {r"abc def ˇ paragraph @@ -649,18 +648,16 @@ mod test { third and - final"}, - Mode::Normal, - ); + final"}) + .await; // goes up once - cx.simulate_keystrokes(["{"]); - cx.assert_state(initial_state, Mode::Normal); + cx.simulate_shared_keystrokes(["{"]).await; + cx.assert_shared_state(initial_state).await; // goes down twice - cx.simulate_keystrokes(["2", "}"]); - cx.assert_state( - indoc! {r"abc + cx.simulate_shared_keystrokes(["2", "}"]).await; + cx.assert_shared_state(indoc! {r"abc def paragraph @@ -669,14 +666,12 @@ mod test { third and - final"}, - Mode::Normal, - ); + final"}) + .await; // goes down over multiple blanks - cx.simulate_keystrokes(["}"]); - cx.assert_state( - indoc! {r"abc + cx.simulate_shared_keystrokes(["}"]).await; + cx.assert_shared_state(indoc! {r"abc def paragraph @@ -685,14 +680,12 @@ mod test { third and - finalˇ"}, - Mode::Normal, - ); + finaˇl"}) + .await; // goes up twice - cx.simulate_keystrokes(["2", "{"]); - cx.assert_state( - indoc! {r"abc + cx.simulate_shared_keystrokes(["2", "{"]).await; + cx.assert_shared_state(indoc! {r"abc def ˇ paragraph @@ -701,8 +694,7 @@ mod test { third and - final"}, - Mode::Normal, - ) + final"}) + .await } } diff --git a/crates/vim/src/test/neovim_backed_test_context.rs b/crates/vim/src/test/neovim_backed_test_context.rs index 9b6bf976ca08a9a4ecf90a907f47772e8f1bdba1..7f9a84b666c4babdacba866bafef1f492f067c19 100644 --- a/crates/vim/src/test/neovim_backed_test_context.rs +++ b/crates/vim/src/test/neovim_backed_test_context.rs @@ -1,9 +1,10 @@ -use std::ops::{Deref, DerefMut}; +use indoc::indoc; +use std::ops::{Deref, DerefMut, Range}; use collections::{HashMap, HashSet}; use gpui::ContextHandle; use language::OffsetRangeExt; -use util::test::marked_text_offsets; +use util::test::{generate_marked_text, marked_text_offsets}; use super::{neovim_connection::NeovimConnection, NeovimBackedBindingTestContext, VimTestContext}; use crate::state::Mode; @@ -112,6 +113,43 @@ impl<'a> NeovimBackedTestContext<'a> { context_handle } + pub async fn assert_shared_state(&mut self, marked_text: &str) { + let neovim = self.neovim_state().await; + if neovim != marked_text { + panic!( + indoc! {"Test is incorrect (currently expected != neovim state) + + # currently expected: + {} + # neovim state: + {} + # zed state: + {}"}, + marked_text, + neovim, + self.editor_state(), + ) + } + self.assert_editor_state(marked_text) + } + + pub async fn neovim_state(&mut self) -> String { + generate_marked_text( + self.neovim.text().await.as_str(), + &vec![self.neovim_selection().await], + true, + ) + } + + async fn neovim_selection(&mut self) -> Range { + let mut neovim_selection = self.neovim.selection().await; + // Zed selections adjust themselves to make the end point visually make sense + if neovim_selection.start > neovim_selection.end { + neovim_selection.start.column += 1; + } + neovim_selection.to_offset(&self.buffer_snapshot()) + } + pub async fn assert_state_matches(&mut self) { assert_eq!( self.neovim.text().await, @@ -120,13 +158,8 @@ impl<'a> NeovimBackedTestContext<'a> { self.assertion_context() ); - let mut neovim_selection = self.neovim.selection().await; - // Zed selections adjust themselves to make the end point visually make sense - if neovim_selection.start > neovim_selection.end { - neovim_selection.start.column += 1; - } - let neovim_selection = neovim_selection.to_offset(&self.buffer_snapshot()); - self.assert_editor_selections(vec![neovim_selection]); + let selections = vec![self.neovim_selection().await]; + self.assert_editor_selections(selections); if let Some(neovim_mode) = self.neovim.mode().await { assert_eq!(neovim_mode, self.mode(), "{}", self.assertion_context(),); diff --git a/crates/vim/test_data/test_start_end_of_paragraph.json b/crates/vim/test_data/test_start_end_of_paragraph.json new file mode 100644 index 0000000000000000000000000000000000000000..0de4d84f50f6cb46ec29ee5ba2d394b07c9105cc --- /dev/null +++ b/crates/vim/test_data/test_start_end_of_paragraph.json @@ -0,0 +1,13 @@ +{"Put":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal"}} +{"Key":"}"} +{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} +{"Key":"{"} +{"Get":{"state":"ˇabc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} +{"Key":"2"} +{"Key":"}"} +{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\nˇ\n\n\nthird and\nfinal","mode":"Normal"}} +{"Key":"}"} +{"Get":{"state":"abc\ndef\n\nparagraph\nthe second\n\n\n\nthird and\nfinaˇl","mode":"Normal"}} +{"Key":"2"} +{"Key":"{"} +{"Get":{"state":"abc\ndef\nˇ\nparagraph\nthe second\n\n\n\nthird and\nfinal","mode":"Normal"}} From e36d5f41c8a29bbb81ae683fa262251c3f103e41 Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Fri, 30 Jun 2023 12:38:28 -0600 Subject: [PATCH 07/29] Fix % when on the last character of the line Contributes: zed-industries/community#682 --- crates/vim/src/motion.rs | 50 ++++++++++++++++++++++++- crates/vim/test_data/test_matching.json | 17 +++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 crates/vim/test_data/test_matching.json diff --git a/crates/vim/src/motion.rs b/crates/vim/src/motion.rs index e8084cb4be13f8ffd3214b50bba1cc071d32193b..07b095dd5e186412d566badc8cbdda5be637130b 100644 --- a/crates/vim/src/motion.rs +++ b/crates/vim/src/motion.rs @@ -525,10 +525,13 @@ fn matching(map: &DisplaySnapshot, display_point: DisplayPoint) -> DisplayPoint if line_end == point { line_end = map.max_point().to_point(map); } - line_end.column = line_end.column.saturating_sub(1); let line_range = map.prev_line_boundary(point).0..line_end; - let ranges = map.buffer_snapshot.bracket_ranges(line_range.clone()); + let visible_line_range = + line_range.start..Point::new(line_range.end.row, line_range.end.column.saturating_sub(1)); + let ranges = map + .buffer_snapshot + .bracket_ranges(visible_line_range.clone()); if let Some(ranges) = ranges { let line_range = line_range.start.to_offset(&map.buffer_snapshot) ..line_range.end.to_offset(&map.buffer_snapshot); @@ -697,4 +700,47 @@ mod test { final"}) .await } + + #[gpui::test] + async fn test_matching(cx: &mut gpui::TestAppContext) { + let mut cx = NeovimBackedTestContext::new(cx).await; + + cx.set_shared_state(indoc! {r"func ˇ(a string) { + do(something(with.and_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state(indoc! {r"func (a stringˇ) { + do(something(with.and_arrays[0, 2])) + }"}) + .await; + + // test it works on the last character of the line + cx.set_shared_state(indoc! {r"func (a string) ˇ{ + do(something(with.and_arrays[0, 2])) + }"}) + .await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state(indoc! {r"func (a string) { + do(something(with.and_arrays[0, 2])) + ˇ}"}) + .await; + + // test it works on immediate nesting + cx.set_shared_state("ˇ{()}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("{()ˇ}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("ˇ{()}").await; + + // test it works on immediate nesting inside braces + cx.set_shared_state("{\n ˇ{()}\n}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("{\n {()ˇ}\n}").await; + + // test it jumps to the next paren on a line + cx.set_shared_state("func ˇboop() {\n}").await; + cx.simulate_shared_keystrokes(["%"]).await; + cx.assert_shared_state("func boop(ˇ) {\n}").await; + } } diff --git a/crates/vim/test_data/test_matching.json b/crates/vim/test_data/test_matching.json new file mode 100644 index 0000000000000000000000000000000000000000..5c8d7529b97e7e86b1c9fb5970aac220d2de4def --- /dev/null +++ b/crates/vim/test_data/test_matching.json @@ -0,0 +1,17 @@ +{"Put":{"state":"func ˇ(a string) {\n do(something(with.and_arrays[0, 2]))\n}"}} +{"Key":"%"} +{"Get":{"state":"func (a stringˇ) {\n do(something(with.and_arrays[0, 2]))\n}","mode":"Normal"}} +{"Put":{"state":"func (a string) ˇ{\ndo(something(with.and_arrays[0, 2]))\n}"}} +{"Key":"%"} +{"Get":{"state":"func (a string) {\ndo(something(with.and_arrays[0, 2]))\nˇ}","mode":"Normal"}} +{"Put":{"state":"ˇ{()}"}} +{"Key":"%"} +{"Get":{"state":"{()ˇ}","mode":"Normal"}} +{"Key":"%"} +{"Get":{"state":"ˇ{()}","mode":"Normal"}} +{"Put":{"state":"{\n ˇ{()}\n}"}} +{"Key":"%"} +{"Get":{"state":"{\n {()ˇ}\n}","mode":"Normal"}} +{"Put":{"state":"func ˇboop() {\n}"}} +{"Key":"%"} +{"Get":{"state":"func boop(ˇ) {\n}","mode":"Normal"}} From b055f594b06bd0ad56dab64b24c5af613f538ddd Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 3 Jul 2023 12:50:17 -0600 Subject: [PATCH 08/29] vim: ctrl-c to exit visual mode Fixes: zed-industries/community#1447 Contributes: zed-industries/community#1089 --- assets/keymaps/vim.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index afee6fcd2e998500ba55e08230473e91e68dbb96..d0190788343619ad9efc62819437793246a3a5a7 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -305,6 +305,10 @@ "vim::PushOperator", "Replace" ], + "ctrl-c": [ + "vim::SwitchMode", + "Normal" + ], "> >": "editor::Indent", "< <": "editor::Outdent" } From fe57e04016c120eb83441b8722febed1f6b4984f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 3 Jul 2023 12:55:41 -0600 Subject: [PATCH 09/29] vim: Allow ^ as a motion Fixes: zed-industries/community#856 --- assets/keymaps/vim.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index d0190788343619ad9efc62819437793246a3a5a7..86e5b51f19cc7f0ba0c72d4caff135776fb047dd 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -35,6 +35,7 @@ "l": "vim::Right", "right": "vim::Right", "$": "vim::EndOfLine", + "^": "vim::FirstNonWhitespace", "shift-g": "vim::EndOfDocument", "w": "vim::NextWordStart", "shift-w": [ @@ -165,7 +166,6 @@ "shift-a": "vim::InsertEndOfLine", "x": "vim::DeleteRight", "shift-x": "vim::DeleteLeft", - "^": "vim::FirstNonWhitespace", "o": "vim::InsertLineBelow", "shift-o": "vim::InsertLineAbove", "~": "vim::ChangeCase", From 0733e8d50f0ffa3339cb03c14b661ca2f4334d9f Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 3 Jul 2023 15:20:10 -0600 Subject: [PATCH 10/29] Remove editor::Cancel binding from vim When you hit in the command palette, it first editor::Cancel because the command palette is also a focused editor; this binding was catching before the `menu::Cancel` that you probably want. From looking at the uses of editor::Cancel it seems like the only way to trigger this is with in an editor. Rather than trying to hook into the existing editor cancel and add vim-specific behaviour, we'll instead take responsibility for binding directly to when necessary. Fixes: zed-industries/community#1347 --- assets/keymaps/vim.json | 10 ++++++++-- crates/vim/src/test.rs | 14 ++++++++++++++ crates/vim/src/test/vim_test_context.rs | 2 ++ crates/vim/src/vim.rs | 23 ++--------------------- 4 files changed, 26 insertions(+), 23 deletions(-) diff --git a/assets/keymaps/vim.json b/assets/keymaps/vim.json index 86e5b51f19cc7f0ba0c72d4caff135776fb047dd..e08ce47caf4b785ca79b3ae9345f49b51919c81d 100644 --- a/assets/keymaps/vim.json +++ b/assets/keymaps/vim.json @@ -93,7 +93,10 @@ ], "ctrl-o": "pane::GoBack", "ctrl-]": "editor::GoToDefinition", - "escape": "editor::Cancel", + "escape": [ + "vim::SwitchMode", + "Normal" + ], "0": "vim::StartOfLine", // When no number operator present, use start of line motion "1": [ "vim::Number", @@ -325,7 +328,10 @@ "bindings": { "tab": "vim::Tab", "enter": "vim::Enter", - "escape": "editor::Cancel" + "escape": [ + "vim::SwitchMode", + "Normal" + ] } } ] diff --git a/crates/vim/src/test.rs b/crates/vim/src/test.rs index b6c5b7ca51c928072c242ee534bc29c29833878b..a6efbd4da1de34447d7165040bd6da32a79faa9c 100644 --- a/crates/vim/src/test.rs +++ b/crates/vim/src/test.rs @@ -4,6 +4,7 @@ mod neovim_connection; mod vim_binding_test_context; mod vim_test_context; +use command_palette::CommandPalette; pub use neovim_backed_binding_test_context::*; pub use neovim_backed_test_context::*; pub use vim_binding_test_context::*; @@ -139,3 +140,16 @@ async fn test_indent_outdent(cx: &mut gpui::TestAppContext) { cx.simulate_keystrokes(["shift-v", "down", ">", ">"]); cx.assert_editor_state("aa\n b«b\n cˇ»c"); } + +#[gpui::test] +async fn test_escape_command_palette(cx: &mut gpui::TestAppContext) { + let mut cx = VimTestContext::new(cx, true).await; + + cx.set_state("aˇbc\n", Mode::Normal); + cx.simulate_keystrokes(["i", "cmd-shift-p"]); + + assert!(cx.workspace(|workspace, _| workspace.modal::().is_some())); + cx.simulate_keystroke("escape"); + assert!(!cx.workspace(|workspace, _| workspace.modal::().is_some())); + cx.assert_state("aˇbc\n", Mode::Insert); +} diff --git a/crates/vim/src/test/vim_test_context.rs b/crates/vim/src/test/vim_test_context.rs index 3e66d6bb1c0e78b581ffdb328d24d06f08814d7f..f9ba577231f5de6317f1194e14a6ad4cf139e06b 100644 --- a/crates/vim/src/test/vim_test_context.rs +++ b/crates/vim/src/test/vim_test_context.rs @@ -21,12 +21,14 @@ impl<'a> VimTestContext<'a> { cx.update(|cx| { search::init(cx); crate::init(cx); + command_palette::init(cx); }); cx.update(|cx| { cx.update_global(|store: &mut SettingsStore, cx| { store.update_user_settings::(cx, |s| *s = Some(enabled)); }); + settings::KeymapFile::load_asset("keymaps/default.json", cx).unwrap(); settings::KeymapFile::load_asset("keymaps/vim.json", cx).unwrap(); }); diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index eae8643cf36e7897a0ed995c41d0bd4177d889f8..2bcc2254eed1d75dd88ec446428c306cd1d677ba 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -12,7 +12,7 @@ mod visual; use anyhow::Result; use collections::CommandPaletteFilter; -use editor::{Bias, Cancel, Editor, EditorMode, Event}; +use editor::{Bias, Editor, EditorMode, Event}; use gpui::{ actions, impl_actions, AppContext, Subscription, ViewContext, ViewHandle, WeakViewHandle, WindowContext, @@ -64,22 +64,6 @@ pub fn init(cx: &mut AppContext) { Vim::update(cx, |vim, cx| vim.push_number(n, cx)); }); - // Editor Actions - cx.add_action(|_: &mut Editor, _: &Cancel, cx| { - // If we are in aren't in normal mode or have an active operator, swap to normal mode - // Otherwise forward cancel on to the editor - let vim = Vim::read(cx); - if vim.state.mode != Mode::Normal || vim.active_operator().is_some() { - WindowContext::defer(cx, |cx| { - Vim::update(cx, |state, cx| { - state.switch_mode(Mode::Normal, false, cx); - }); - }); - } else { - cx.propagate_action(); - } - }); - cx.add_action(|_: &mut Workspace, _: &Tab, cx| { Vim::active_editor_input_ignored(" ".into(), cx) }); @@ -109,10 +93,7 @@ pub fn observe_keystrokes(cx: &mut WindowContext) { cx.observe_keystrokes(|_keystroke, _result, handled_by, cx| { if let Some(handled_by) = handled_by { // Keystroke is handled by the vim system, so continue forward - // Also short circuit if it is the special cancel action - if handled_by.namespace() == "vim" - || (handled_by.namespace() == "editor" && handled_by.name() == "Cancel") - { + if handled_by.namespace() == "vim" { return true; } } From 0d18b72cf86d74287f4cfc61da5ecaacd82498cb Mon Sep 17 00:00:00 2001 From: Conrad Irwin Date: Mon, 3 Jul 2023 23:52:11 -0600 Subject: [PATCH 11/29] vim: Further improve ~ handling Now works with Visual{line} mode, collapses selections like nvim, and doesn't fall off the end of the line. --- crates/vim/src/normal/case.rs | 96 +++++++++++++++------- crates/vim/src/test/neovim_connection.rs | 16 +++- crates/vim/test_data/test_change_case.json | 18 ++++ 3 files changed, 99 insertions(+), 31 deletions(-) create mode 100644 crates/vim/test_data/test_change_case.json diff --git a/crates/vim/src/normal/case.rs b/crates/vim/src/normal/case.rs index ba527af0bb0b994f571dad2cf5a5ed466a9e3b98..b3e101262d02a54f31f668dd665bcdad6a59875e 100644 --- a/crates/vim/src/normal/case.rs +++ b/crates/vim/src/normal/case.rs @@ -1,29 +1,51 @@ +use editor::scroll::autoscroll::Autoscroll; use gpui::ViewContext; -use language::Point; +use language::{Bias, Point}; use workspace::Workspace; -use crate::{motion::Motion, normal::ChangeCase, Vim}; +use crate::{normal::ChangeCase, state::Mode, Vim}; pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext) { Vim::update(cx, |vim, cx| { - let count = vim.pop_number_operator(cx); + let count = vim.pop_number_operator(cx).unwrap_or(1) as u32; vim.update_active_editor(cx, |editor, cx| { - editor.set_clip_at_line_ends(false, cx); - editor.transact(cx, |editor, cx| { - editor.change_selections(None, cx, |s| { - s.move_with(|map, selection| { - if selection.start == selection.end { - Motion::Right.expand_selection(map, selection, count, true); + let mut ranges = Vec::new(); + let mut cursor_positions = Vec::new(); + let snapshot = editor.buffer().read(cx).snapshot(cx); + for selection in editor.selections.all::(cx) { + match vim.state.mode { + Mode::Visual { line: true } => { + let start = Point::new(selection.start.row, 0); + let end = + Point::new(selection.end.row, snapshot.line_len(selection.end.row)); + ranges.push(start..end); + cursor_positions.push(start..start); + } + Mode::Visual { line: false } => { + ranges.push(selection.start..selection.end); + cursor_positions.push(selection.start..selection.start); + } + Mode::Insert | Mode::Normal => { + let start = selection.start; + let mut end = start; + for _ in 0..count { + end = snapshot.clip_point(end + Point::new(0, 1), Bias::Right); } - }) - }); - let selections = editor.selections.all::(cx); - for selection in selections.into_iter().rev() { + ranges.push(start..end); + + if end.column == snapshot.line_len(end.row) { + end = snapshot.clip_point(end - Point::new(0, 1), Bias::Left); + } + cursor_positions.push(end..end) + } + } + } + editor.transact(cx, |editor, cx| { + for range in ranges.into_iter().rev() { let snapshot = editor.buffer().read(cx).snapshot(cx); editor.buffer().update(cx, |buffer, cx| { - let range = selection.start..selection.end; let text = snapshot - .text_for_range(selection.start..selection.end) + .text_for_range(range.start..range.end) .flat_map(|s| s.chars()) .flat_map(|c| { if c.is_lowercase() { @@ -37,28 +59,46 @@ pub fn change_case(_: &mut Workspace, _: &ChangeCase, cx: &mut ViewContext Date: Fri, 7 Jul 2023 16:30:19 +0200 Subject: [PATCH 12/29] Add tooltip to recent projects button Z-2545 --- crates/collab_ui/src/collab_titlebar_item.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 73450e7c7d9974f83d78d6c0ad7b4e26ec4fefa3..606a77cd2f6b59675818118e7ffc85873f73b999 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -229,15 +229,23 @@ impl CollabTitlebarItem { let mut ret = Flex::row().with_child( Stack::new() .with_child( - MouseEventHandler::::new(0, cx, |mouse_state, _| { + MouseEventHandler::::new(0, cx, |mouse_state, cx| { let style = project_style .in_state(self.project_popover.is_some()) .style_for(mouse_state); + enum RecentProjectsTooltip {} Label::new(name, style.text.clone()) .contained() .with_style(style.container) .aligned() .left() + .with_tooltip::( + 0, + "Recent projects".into(), + None, + theme.tooltip.clone(), + cx, + ) .into_any_named("title-project-name") }) .with_cursor_style(CursorStyle::PointingHand) From 66bf56fc4f571c94486c5e2a1552aebf66f5f797 Mon Sep 17 00:00:00 2001 From: Julia Date: Thu, 6 Jul 2023 17:18:45 -0400 Subject: [PATCH 13/29] Prevent duplicate instances by coordinating via a socket --- crates/cli/src/main.rs | 1 + crates/zed/src/main.rs | 9 +++- crates/zed/src/only_instance.rs | 82 +++++++++++++++++++++++++++++++++ crates/zed/src/zed.rs | 1 + 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 crates/zed/src/only_instance.rs diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index bdf677512c4b0326cf2bc0e513a00bbb563f0a02..2f742814a8dbfdfeb7719fbe906858448d8253f8 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -201,6 +201,7 @@ impl Bundle { self.zed_version_string() ); } + Self::LocalPath { executable, .. } => { let executable_parent = executable .parent() diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 3da8c24617909abb00cd6aa19e1d6adff6cad010..5eed301367481bb0b747631cbee7d062d9c6febe 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -57,8 +57,9 @@ use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{item::ItemHandle, notifications::NotifyResultExt, AppState, Workspace}; use zed::{ - assets::Assets, build_window_options, handle_keymap_file_changes, initialize_workspace, - languages, menus, + assets::Assets, + build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, + only_instance::{ensure_only_instance, IsOnlyInstance}, }; fn main() { @@ -66,6 +67,10 @@ fn main() { init_paths(); init_logger(); + if ensure_only_instance() != IsOnlyInstance::Yes { + return; + } + log::info!("========== starting zed =========="); let mut app = gpui::App::new(Assets).unwrap(); diff --git a/crates/zed/src/only_instance.rs b/crates/zed/src/only_instance.rs new file mode 100644 index 0000000000000000000000000000000000000000..c1358f7a336ec955c19137187232d58d8bc0652c --- /dev/null +++ b/crates/zed/src/only_instance.rs @@ -0,0 +1,82 @@ +use std::{ + io::{Read, Write}, + net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener, TcpStream}, + thread, + time::Duration, +}; + +const PORT: u16 = 43739; +const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); +const ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(LOCALHOST, PORT)); +const INSTANCE_HANDSHAKE: &str = "Zed Editor Instance Running"; +const CONNECT_TIMEOUT: Duration = Duration::from_millis(10); +const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35); +const SEND_TIMEOUT: Duration = Duration::from_millis(20); + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IsOnlyInstance { + Yes, + No, +} + +pub fn ensure_only_instance() -> IsOnlyInstance { + if check_got_handshake() { + return IsOnlyInstance::No; + } + + let listener = match TcpListener::bind(ADDRESS) { + Ok(listener) => listener, + + Err(err) => { + log::warn!("Error binding to single instance port: {err}"); + if check_got_handshake() { + return IsOnlyInstance::No; + } + + // Avoid failing to start when some other application by chance already has + // a claim on the port. This is sub-par as any other instance that gets launched + // will be unable to communicate with this instance and will duplicate + log::warn!("Backup handshake request failed, continuing without handshake"); + return IsOnlyInstance::Yes; + } + }; + + thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = match stream { + Ok(stream) => stream, + Err(_) => return, + }; + + _ = stream.set_nodelay(true); + _ = stream.set_read_timeout(Some(SEND_TIMEOUT)); + _ = stream.write_all(INSTANCE_HANDSHAKE.as_bytes()); + } + }); + + IsOnlyInstance::Yes +} + +fn check_got_handshake() -> bool { + match TcpStream::connect_timeout(&ADDRESS, CONNECT_TIMEOUT) { + Ok(mut stream) => { + let mut buf = vec![0u8; INSTANCE_HANDSHAKE.len()]; + + stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap(); + if let Err(err) = stream.read_exact(&mut buf) { + log::warn!("Connected to single instance port but failed to read: {err}"); + return false; + } + + if buf == INSTANCE_HANDSHAKE.as_bytes() { + log::info!("Got instance handshake"); + return true; + } + + log::warn!("Got wrong instance handshake value"); + false + } + + Err(_) => false, + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 0df16f4bab13fa56ed3211e462d8203be161c331..09bdbf65beef1f6f7825b112f485d0b5eec35205 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1,6 +1,7 @@ pub mod assets; pub mod languages; pub mod menus; +pub mod only_instance; #[cfg(any(test, feature = "test-support"))] pub mod test; From b70b76029e17e1cb96d9d008755bb48fd682df5c Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 7 Jul 2023 14:18:43 -0400 Subject: [PATCH 14/29] Use different port and handshake for different release channels --- crates/db/src/db.rs | 3 +-- crates/zed/src/only_instance.rs | 33 +++++++++++++++++++++++++-------- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 798dfbc17f70173095dc9671543a60c2ef381732..7b4aa74a80761dad70dcca1283fdd25d1156997a 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -41,8 +41,7 @@ const FALLBACK_DB_NAME: &'static str = "FALLBACK_MEMORY_DB"; const DB_FILE_NAME: &'static str = "db.sqlite"; lazy_static::lazy_static! { - // !!!!!!! CHANGE BACK TO DEFAULT FALSE BEFORE SHIPPING - static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()); + pub static ref ZED_STATELESS: bool = std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()); pub static ref BACKUP_DB_PATH: RwLock> = RwLock::new(None); pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false); } diff --git a/crates/zed/src/only_instance.rs b/crates/zed/src/only_instance.rs index c1358f7a336ec955c19137187232d58d8bc0652c..0450b5908ba30a04779c06820e79b7139e7119d7 100644 --- a/crates/zed/src/only_instance.rs +++ b/crates/zed/src/only_instance.rs @@ -5,14 +5,31 @@ use std::{ time::Duration, }; -const PORT: u16 = 43739; +use util::channel::ReleaseChannel; + const LOCALHOST: Ipv4Addr = Ipv4Addr::new(127, 0, 0, 1); -const ADDRESS: SocketAddr = SocketAddr::V4(SocketAddrV4::new(LOCALHOST, PORT)); -const INSTANCE_HANDSHAKE: &str = "Zed Editor Instance Running"; const CONNECT_TIMEOUT: Duration = Duration::from_millis(10); const RECEIVE_TIMEOUT: Duration = Duration::from_millis(35); const SEND_TIMEOUT: Duration = Duration::from_millis(20); +fn address() -> SocketAddr { + let port = match *util::channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => 43737, + ReleaseChannel::Preview => 43738, + ReleaseChannel::Stable => 43739, + }; + + SocketAddr::V4(SocketAddrV4::new(LOCALHOST, port)) +} + +fn instance_handshake() -> &'static str { + match *util::channel::RELEASE_CHANNEL { + ReleaseChannel::Dev => "Zed Editor Dev Instance Running", + ReleaseChannel::Preview => "Zed Editor Preview Instance Running", + ReleaseChannel::Stable => "Zed Editor Stable Instance Running", + } +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum IsOnlyInstance { Yes, @@ -24,7 +41,7 @@ pub fn ensure_only_instance() -> IsOnlyInstance { return IsOnlyInstance::No; } - let listener = match TcpListener::bind(ADDRESS) { + let listener = match TcpListener::bind(address()) { Ok(listener) => listener, Err(err) => { @@ -50,7 +67,7 @@ pub fn ensure_only_instance() -> IsOnlyInstance { _ = stream.set_nodelay(true); _ = stream.set_read_timeout(Some(SEND_TIMEOUT)); - _ = stream.write_all(INSTANCE_HANDSHAKE.as_bytes()); + _ = stream.write_all(instance_handshake().as_bytes()); } }); @@ -58,9 +75,9 @@ pub fn ensure_only_instance() -> IsOnlyInstance { } fn check_got_handshake() -> bool { - match TcpStream::connect_timeout(&ADDRESS, CONNECT_TIMEOUT) { + match TcpStream::connect_timeout(&address(), CONNECT_TIMEOUT) { Ok(mut stream) => { - let mut buf = vec![0u8; INSTANCE_HANDSHAKE.len()]; + let mut buf = vec![0u8; instance_handshake().len()]; stream.set_read_timeout(Some(RECEIVE_TIMEOUT)).unwrap(); if let Err(err) = stream.read_exact(&mut buf) { @@ -68,7 +85,7 @@ fn check_got_handshake() -> bool { return false; } - if buf == INSTANCE_HANDSHAKE.as_bytes() { + if buf == instance_handshake().as_bytes() { log::info!("Got instance handshake"); return true; } From caa29d57c2d82fbbcdc2f7d7ef6ec1d39969767c Mon Sep 17 00:00:00 2001 From: Julia Date: Fri, 7 Jul 2023 14:19:13 -0400 Subject: [PATCH 15/29] Avoid checking for duplicate instance when local DB is disabled --- crates/zed/src/only_instance.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/zed/src/only_instance.rs b/crates/zed/src/only_instance.rs index 0450b5908ba30a04779c06820e79b7139e7119d7..a8c4b30816dfbbed4ae5c186d2d0e43722245293 100644 --- a/crates/zed/src/only_instance.rs +++ b/crates/zed/src/only_instance.rs @@ -37,6 +37,10 @@ pub enum IsOnlyInstance { } pub fn ensure_only_instance() -> IsOnlyInstance { + if *db::ZED_STATELESS { + return IsOnlyInstance::Yes; + } + if check_got_handshake() { return IsOnlyInstance::No; } From 52a497be21d9a19ae9136d13ab524847d3b84daf Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Sat, 8 Jul 2023 18:03:18 -0400 Subject: [PATCH 16/29] Remove code block for GitHub release notes Discord can directly render the Markdown now. --- .github/workflows/release_actions.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release_actions.yml b/.github/workflows/release_actions.yml index 4ccab09cbe10c56f49d43e08d6483b28846021cb..71909ae1771637ed099e532266d740dfe3361a2f 100644 --- a/.github/workflows/release_actions.yml +++ b/.github/workflows/release_actions.yml @@ -16,8 +16,4 @@ jobs: Restart your Zed or head to https://zed.dev/releases/stable/latest to grab it. - ```md - # Changelog - ${{ github.event.release.body }} - ``` From 6e24ded2bcbab2b0798c8c1ed3b88165a30c5c47 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:20:12 +0200 Subject: [PATCH 17/29] collab_ui: Add tooltip to branches popover (#2695) Z-2554 Release Notes: - N/A --- crates/collab_ui/src/collab_titlebar_item.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 73450e7c7d9974f83d78d6c0ad7b4e26ec4fefa3..ed3315ab5af9bb7c71d4c5d8abba74c0d07d92ec 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -264,7 +264,8 @@ impl CollabTitlebarItem { MouseEventHandler::::new( 0, cx, - |mouse_state, _| { + |mouse_state, cx| { + enum BranchPopoverTooltip {} let style = git_style .in_state(self.branch_popover.is_some()) .style_for(mouse_state); @@ -274,6 +275,13 @@ impl CollabTitlebarItem { .with_margin_right(item_spacing) .aligned() .left() + .with_tooltip::( + 0, + "Recent branches".into(), + None, + theme.tooltip.clone(), + cx, + ) .into_any_named("title-project-branch") }, ) From 6c8cb6b2a9f8dbebb7e31df9035940b5ffd7659f Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 5 Jul 2023 10:54:28 +0200 Subject: [PATCH 18/29] project_search: display result count on cmd-enter It also focuses the first result (just like a normal enter). --- crates/search/src/project_search.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/search/src/project_search.rs b/crates/search/src/project_search.rs index 135194df6a2f5a22c1ed001a725488ae2c003437..ebd504d02c2334aa6876a478937718cb1aa4d496 100644 --- a/crates/search/src/project_search.rs +++ b/crates/search/src/project_search.rs @@ -675,6 +675,9 @@ impl ProjectSearchView { if match_ranges.is_empty() { self.active_match_index = None; } else { + self.active_match_index = Some(0); + self.select_match(Direction::Next, cx); + self.update_match_index(cx); let prev_search_id = mem::replace(&mut self.search_id, self.model.read(cx).search_id); let is_new_search = self.search_id != prev_search_id; self.results_editor.update(cx, |editor, cx| { From 3318896ad964824e64a0831eab7fa48ce04d8aa1 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:29:30 +0200 Subject: [PATCH 19/29] Display key bind of a modal project picker --- crates/collab_ui/src/collab_titlebar_item.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 606a77cd2f6b59675818118e7ffc85873f73b999..3d4d314902b4e1d274cb07b6b93ea23760e9ff85 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -242,7 +242,7 @@ impl CollabTitlebarItem { .with_tooltip::( 0, "Recent projects".into(), - None, + Some(Box::new(recent_projects::OpenRecent)), theme.tooltip.clone(), cx, ) From f0cddeb478ee26892cd693c105c0ad874aac43be Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 10 Jul 2023 10:09:59 -0400 Subject: [PATCH 20/29] Update zoom icons --- assets/icons/radix/maximize.svg | 4 ++++ assets/icons/radix/minimize.svg | 4 ++++ styles/src/style_tree/assistant.ts | 4 ++-- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 assets/icons/radix/maximize.svg create mode 100644 assets/icons/radix/minimize.svg diff --git a/assets/icons/radix/maximize.svg b/assets/icons/radix/maximize.svg new file mode 100644 index 0000000000000000000000000000000000000000..f37f6a2087f968728170539b379206cca7551b0e --- /dev/null +++ b/assets/icons/radix/maximize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/icons/radix/minimize.svg b/assets/icons/radix/minimize.svg new file mode 100644 index 0000000000000000000000000000000000000000..ec78f152e13eda0c887a18b99b585d0c65acc8a8 --- /dev/null +++ b/assets/icons/radix/minimize.svg @@ -0,0 +1,4 @@ + + + + diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index 802e4139cb60aaac079a00550fb7713d0abf7a8a..3205268f5314319770e45b809ce26ff0d79272db 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -80,10 +80,10 @@ export default function assistant(theme: ColorScheme): any { icon: "icons/radix/magic-wand.svg", }), zoom_in_button: tab_bar_button(theme, { - icon: "icons/radix/enter-full-screen.svg", + icon: "icons/radix/maximize.svg", }), zoom_out_button: tab_bar_button(theme, { - icon: "icons/radix/exit-full-screen.svg", + icon: "icons/radix/minimize.svg", }), plus_button: tab_bar_button(theme, { icon: "icons/radix/plus.svg", From 9ffe220def57349e6655e95f1cdbe26bd0bbe153 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 10 Jul 2023 10:24:24 -0400 Subject: [PATCH 21/29] Update tab_bar_button.ts --- styles/src/component/tab_bar_button.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/styles/src/component/tab_bar_button.ts b/styles/src/component/tab_bar_button.ts index 2b657e8b37ff2a4860a51e05dc003c2d3f77368c..0c43e7010e5469c10f959e00f4df8d177963392f 100644 --- a/styles/src/component/tab_bar_button.ts +++ b/styles/src/component/tab_bar_button.ts @@ -1,4 +1,4 @@ -import { ColorScheme, StyleSets } from "../common" +import { Theme, StyleSets } from "../common" import { interactive } from "../element" import { InteractiveState } from "../element/interactive" import { background, foreground } from "../style_tree/components" @@ -12,7 +12,7 @@ type TabBarButtonProps = TabBarButtonOptions & { state?: Partial>> } -export function tab_bar_button(theme: ColorScheme, { icon, color = "base" }: TabBarButtonProps) { +export function tab_bar_button(theme: Theme, { icon, color = "base" }: TabBarButtonProps) { const button_spacing = 8 return ( From 273b9e1636bd63b6ec0ea113c6562afe05cceb0d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Mon, 10 Jul 2023 10:44:39 -0400 Subject: [PATCH 22/29] Avoid overlapping the scrollbar --- styles/src/style_tree/assistant.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/styles/src/style_tree/assistant.ts b/styles/src/style_tree/assistant.ts index adec3dee62d83211ddf5b61f78461378c7f60655..cfc1f8d813648654a8fee608ea4d00dc30893b75 100644 --- a/styles/src/style_tree/assistant.ts +++ b/styles/src/style_tree/assistant.ts @@ -47,7 +47,7 @@ export default function assistant(): any { { ...text(theme.highest, "mono", color, { size: "xs" }), background: background(theme.highest, "on", "default"), - margin: { top: 12, right: 8 }, + margin: { top: 12, right: 20 }, padding: { right: 4, left: 4, top: 1, bottom: 1 }, corner_radius: 6, } From e00e73f60848334006c2a42a1364291ba40b8e25 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:18:12 +0200 Subject: [PATCH 23/29] branches: Add a modal branch list. Extract branch list into a separate vcs_menu crate akin to recent_projects. Add current bind for a modal branch to branch popover's tooltip. Z-2555 --- Cargo.lock | 14 ++++++++ Cargo.toml | 1 + assets/keymaps/default.json | 1 + assets/keymaps/textmate.json | 1 + crates/collab_ui/Cargo.toml | 1 + crates/collab_ui/src/collab_titlebar_item.rs | 9 ++--- crates/collab_ui/src/collab_ui.rs | 3 +- crates/vcs_menu/Cargo.toml | 16 +++++++++ .../branch_list.rs => vcs_menu/src/lib.rs} | 36 +++++++++++++++++-- 9 files changed, 71 insertions(+), 11 deletions(-) create mode 100644 crates/vcs_menu/Cargo.toml rename crates/{collab_ui/src/branch_list.rs => vcs_menu/src/lib.rs} (89%) diff --git a/Cargo.lock b/Cargo.lock index 60ed830683a5f91d0c8f4806b5d6758b2afbc31e..ba24a756d1433304e85a4bf233d0193a4bd38094 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1491,6 +1491,7 @@ dependencies = [ "theme", "theme_selector", "util", + "vcs_menu", "workspace", "zed-actions", ] @@ -8377,6 +8378,19 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "vcs_menu" +version = "0.1.0" +dependencies = [ + "anyhow", + "fuzzy", + "gpui", + "picker", + "theme", + "util", + "workspace", +] + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 1708ccfc0a81ec92ac626ef2a6816c55e16a44df..575787196211bcc6e94dfccd3952ff15ddabdb77 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ members = [ "crates/theme_selector", "crates/util", "crates/vim", + "crates/vcs_menu", "crates/workspace", "crates/welcome", "crates/xtask", diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index 6fc06198fe8b24bace855b005d6d113f26555304..8c3a1f407cb8baaeff50ea761205d689a773f081 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -39,6 +39,7 @@ "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" diff --git a/assets/keymaps/textmate.json b/assets/keymaps/textmate.json index 591d6e443fec6e362a48dd40a6912a64f198732a..1f28c05158a9bee72cb697368d8bc114cec1b046 100644 --- a/assets/keymaps/textmate.json +++ b/assets/keymaps/textmate.json @@ -2,6 +2,7 @@ { "bindings": { "cmd-shift-o": "projects::OpenRecent", + "cmd-shift-b": "branches::OpenRecent", "cmd-alt-tab": "project_panel::ToggleFocus" } }, diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index f81885c07a02fbc526d7c675762f3468db571d50..4a38c2691cc602e018d77d9ab89fb88f610433eb 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -39,6 +39,7 @@ recent_projects = {path = "../recent_projects"} settings = { path = "../settings" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } +vcs_menu = { path = "../vcs_menu" } util = { path = "../util" } workspace = { path = "../workspace" } zed-actions = {path = "../zed-actions"} diff --git a/crates/collab_ui/src/collab_titlebar_item.rs b/crates/collab_ui/src/collab_titlebar_item.rs index 57e3ea711d3a8861979e4d50535c719625ec035b..6cfc9d8e30e0dfc7bf97f0ff8e8b94a4722e2b44 100644 --- a/crates/collab_ui/src/collab_titlebar_item.rs +++ b/crates/collab_ui/src/collab_titlebar_item.rs @@ -1,8 +1,5 @@ use crate::{ - branch_list::{build_branch_list, BranchList}, - contact_notification::ContactNotification, - contacts_popover, - face_pile::FacePile, + contact_notification::ContactNotification, contacts_popover, face_pile::FacePile, toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing, }; @@ -27,6 +24,7 @@ use recent_projects::{build_recent_projects, RecentProjects}; use std::{ops::Range, sync::Arc}; use theme::{AvatarStyle, Theme}; use util::ResultExt; +use vcs_menu::{build_branch_list, BranchList, OpenRecent as ToggleVcsMenu}; use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB}; const MAX_PROJECT_NAME_LENGTH: usize = 40; @@ -37,7 +35,6 @@ actions!( [ ToggleContactsMenu, ToggleUserMenu, - ToggleVcsMenu, ToggleProjectMenu, SwitchBranch, ShareProject, @@ -286,7 +283,7 @@ impl CollabTitlebarItem { .with_tooltip::( 0, "Recent branches".into(), - None, + Some(Box::new(ToggleVcsMenu)), theme.tooltip.clone(), cx, ) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 26d9c70a4378af9e8ff90fa4a0af8babf5a45539..76f2e265711b079c00bab6ce3e2677e48d716030 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,4 +1,3 @@ -mod branch_list; mod collab_titlebar_item; mod contact_finder; mod contact_list; @@ -29,7 +28,7 @@ actions!( ); pub fn init(app_state: &Arc, cx: &mut AppContext) { - branch_list::init(cx); + vcs_menu::init(cx); collab_titlebar_item::init(cx); contact_list::init(cx); contact_finder::init(cx); diff --git a/crates/vcs_menu/Cargo.toml b/crates/vcs_menu/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..4ddf1214d0db179f8033547d31dfa2960b254ef5 --- /dev/null +++ b/crates/vcs_menu/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "vcs_menu" +version = "0.1.0" +edition = "2021" +publish = false +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fuzzy = {path = "../fuzzy"} +gpui = {path = "../gpui"} +picker = {path = "../picker"} +util = {path = "../util"} +theme = {path = "../theme"} +workspace = {path = "../workspace"} + +anyhow.workspace = true diff --git a/crates/collab_ui/src/branch_list.rs b/crates/vcs_menu/src/lib.rs similarity index 89% rename from crates/collab_ui/src/branch_list.rs rename to crates/vcs_menu/src/lib.rs index 16fefbd2ebc135a72e056a75e7a2486c7704cab8..b5b1036b36c684c34576e4ef3d0987cbbe3ab387 100644 --- a/crates/collab_ui/src/branch_list.rs +++ b/crates/vcs_menu/src/lib.rs @@ -1,15 +1,17 @@ -use anyhow::{anyhow, bail}; +use anyhow::{anyhow, bail, Result}; use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle}; +use gpui::{actions, elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle}; use picker::{Picker, PickerDelegate, PickerEvent}; use std::{ops::Not, sync::Arc}; use util::ResultExt; use workspace::{Toast, Workspace}; +actions!(branches, [OpenRecent]); + pub fn init(cx: &mut AppContext) { Picker::::init(cx); + cx.add_async_action(toggle); } - pub type BranchList = Picker; pub fn build_branch_list( @@ -28,6 +30,34 @@ pub fn build_branch_list( .with_theme(|theme| theme.picker.clone()) } +fn toggle( + _: &mut Workspace, + _: &OpenRecent, + cx: &mut ViewContext, +) -> Option>> { + Some(cx.spawn(|workspace, mut cx| async move { + workspace.update(&mut cx, |workspace, cx| { + workspace.toggle_modal(cx, |_, cx| { + let workspace = cx.handle(); + cx.add_view(|cx| { + Picker::new( + BranchListDelegate { + matches: vec![], + workspace, + selected_index: 0, + last_query: String::default(), + }, + cx, + ) + .with_theme(|theme| theme.picker.clone()) + .with_max_size(800., 1200.) + }) + }); + })?; + Ok(()) + })) +} + pub struct BranchListDelegate { matches: Vec, workspace: ViewHandle, From a6d713eb3d476e56aec450d338167d3e6d25822d Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Jul 2023 17:44:23 +0200 Subject: [PATCH 24/29] editor: Keep scrollbar up if there are selections Z-2556 --- crates/editor/src/element.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e96f1efe9263f274d66355297e01441ce7474f03..bd662c039be1d3594f0d1272b5af887ec3f469fb 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2149,6 +2149,9 @@ impl Element for EditorElement { ShowScrollbar::Auto => { // Git (is_singleton && scrollbar_settings.git_diff && snapshot.buffer_snapshot.has_git_diffs()) + || + // Selections + (is_singleton && scrollbar_settings.selections && !highlighted_ranges.is_empty()) // Scrollmanager || editor.scroll_manager.scrollbars_visible() } From 4f606798610e513924a3bd055b90f4a0de378b6b Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Mon, 10 Jul 2023 22:51:04 +0200 Subject: [PATCH 25/29] Highlight only search results --- crates/editor/src/editor.rs | 41 ++++++++++++++++++++++++++++++++++++ crates/editor/src/element.rs | 12 ++++++----- crates/editor/src/items.rs | 2 +- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8d7b8ffad6d2804a59affb0935d06306695693e3..98fb887ffd0c0785783bb8cd1f8ed59bc93dcb44 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7222,6 +7222,47 @@ impl Editor { } results } + pub fn background_highlights_in_range_for_key( + &self, + search_range: Range, + display_snapshot: &DisplaySnapshot, + theme: &Theme, + ) -> Vec<(Range, Color)> { + let mut results = Vec::new(); + let buffer = &display_snapshot.buffer_snapshot; + let Some((color_fetcher, ranges)) = self.background_highlights + .get(&TypeId::of::()) else { + return vec![]; + }; + + let color = color_fetcher(theme); + let start_ix = match ranges.binary_search_by(|probe| { + let cmp = probe.end.cmp(&search_range.start, buffer); + if cmp.is_gt() { + Ordering::Greater + } else { + Ordering::Less + } + }) { + Ok(i) | Err(i) => i, + }; + for range in &ranges[start_ix..] { + if range.start.cmp(&search_range.end, buffer).is_ge() { + break; + } + let start = range + .start + .to_point(buffer) + .to_display_point(display_snapshot); + let end = range + .end + .to_point(buffer) + .to_display_point(display_snapshot); + results.push((start..end, color)) + } + + results + } pub fn highlight_text( &mut self, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e96f1efe9263f274d66355297e01441ce7474f03..e79110c81e5e694b525f406e1cc44170b79964e2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1086,11 +1086,13 @@ impl EditorElement { }) } }; - for (row, _) in &editor.background_highlights_in_range( - start_anchor..end_anchor, - &layout.position_map.snapshot, - &theme, - ) { + for (row, _) in &editor + .background_highlights_in_range_for_key::( + start_anchor..end_anchor, + &layout.position_map.snapshot, + &theme, + ) + { let start_display = row.start; let end_display = row.end; diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 74b8e0ddb68d81f3d00fafa518cfbff4bc4c71b6..431ccf0bfe44f2c1370fd07e8f88962fc82b44da 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -883,7 +883,7 @@ impl ProjectItem for Editor { } } -enum BufferSearchHighlights {} +pub(crate) enum BufferSearchHighlights {} impl SearchableItem for Editor { type Match = Range; From e83afdc5abcea6cd3a78c6bd0a41a9c6c1de2360 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Jul 2023 09:31:08 +0200 Subject: [PATCH 26/29] Rename background_highlights_in_range_for_key to background_highlights_in_range_for --- crates/editor/src/editor.rs | 2 +- crates/editor/src/element.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 98fb887ffd0c0785783bb8cd1f8ed59bc93dcb44..28edd2a460f6b17e41e152991fd7b67b2d48f60a 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7222,7 +7222,7 @@ impl Editor { } results } - pub fn background_highlights_in_range_for_key( + pub fn background_highlights_in_range_for( &self, search_range: Range, display_snapshot: &DisplaySnapshot, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index e79110c81e5e694b525f406e1cc44170b79964e2..c07d18767ce3ea5c565f5bcc3ed0fa4b2f336998 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1087,7 +1087,7 @@ impl EditorElement { } }; for (row, _) in &editor - .background_highlights_in_range_for_key::( + .background_highlights_in_range_for::( start_anchor..end_anchor, &layout.position_map.snapshot, &theme, From 91832c8cd8de4743a5c8dad87005a67d9601d7e5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 11 Jul 2023 13:20:02 +0300 Subject: [PATCH 27/29] Fix language servers improper restarts Language servers mixed `initialization_options` from hardcodes and user settings, fix that to ensure we restart servers on their settings changes only. --- crates/language/src/language.rs | 28 +++++++++++++++++++++++++++- crates/project/src/project.rs | 17 +++++------------ 2 files changed, 32 insertions(+), 13 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index e8450344b8a627994cbe173a52c900339aa93cf3..d186bf630d8f19d6d892b46cf6feef5d40c2e936 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -90,7 +90,8 @@ pub struct LanguageServerName(pub Arc); /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, - pub initialization_options: Option, + initialization_options: Option, + initialization_overrides: Mutex>, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, @@ -109,6 +110,7 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, initialization_options, + initialization_overrides: Mutex::new(None), disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, @@ -208,6 +210,30 @@ impl CachedLspAdapter { ) -> Option { self.adapter.label_for_symbol(name, kind, language).await } + + pub fn update_initialization_overrides(&self, new: Option<&Value>) -> bool { + let mut current = self.initialization_overrides.lock(); + if current.as_ref() != new { + *current = new.cloned(); + true + } else { + false + } + } + + pub fn initialization_options(&self) -> Option { + let initialization_options = self.initialization_options.as_ref(); + let override_options = self.initialization_overrides.lock().clone(); + match (initialization_options, override_options) { + (None, override_options) => override_options, + (initialization_options, None) => initialization_options.cloned(), + (Some(initialization_options), Some(override_options)) => { + let mut initialization_options = initialization_options.clone(); + merge_json_value_into(override_options, &mut initialization_options); + Some(initialization_options) + } + } + } } pub trait LspAdapterDelegate: Send + Sync { diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 81db0c7ed7d3b6ffe61df7104e53796d9dd43b54..dc4c8852ddc48751adc4f041d1cc216bcbf2eb92 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -78,8 +78,8 @@ use std::{ use terminals::Terminals; use text::Anchor; use util::{ - debug_panic, defer, http::HttpClient, merge_json_value_into, - paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _, + debug_panic, defer, http::HttpClient, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, + TryFutureExt as _, }; pub use fs::*; @@ -800,7 +800,7 @@ impl Project { .lsp .get(&adapter.name.0) .and_then(|s| s.initialization_options.as_ref()); - if adapter.initialization_options.as_ref() != new_lsp_settings { + if adapter.update_initialization_overrides(new_lsp_settings) { language_servers_to_restart.push((worktree, Arc::clone(language))); } } @@ -2545,20 +2545,13 @@ impl Project { let project_settings = settings::get::(cx); let lsp = project_settings.lsp.get(&adapter.name.0); let override_options = lsp.map(|s| s.initialization_options.clone()).flatten(); - - let mut initialization_options = adapter.initialization_options.clone(); - match (&mut initialization_options, override_options) { - (Some(initialization_options), Some(override_options)) => { - merge_json_value_into(override_options, initialization_options); - } - (None, override_options) => initialization_options = override_options, - _ => {} - } + adapter.update_initialization_overrides(override_options.as_ref()); let server_id = pending_server.server_id; let container_dir = pending_server.container_dir.clone(); let state = LanguageServerState::Starting({ let adapter = adapter.clone(); + let initialization_options = adapter.initialization_options(); let server_name = adapter.name.0.clone(); let languages = self.languages.clone(); let language = language.clone(); From 748e7af5a2c77e572474d836f9a4292dfd589780 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 11 Jul 2023 17:06:02 +0300 Subject: [PATCH 28/29] Add a test --- crates/editor/src/editor_tests.rs | 160 +++++++++++++++++++++++++- crates/editor/src/element.rs | 4 +- crates/editor/src/inlay_hint_cache.rs | 10 +- crates/language/src/language.rs | 6 + 4 files changed, 168 insertions(+), 12 deletions(-) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 9e726d6cc45437bbaf287a1e82849ad66a60b9a9..7b36287dca5ab84a695c9ecb8c701cf84c71a043 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -22,7 +22,10 @@ use language::{ BracketPairConfig, FakeLspAdapter, LanguageConfig, LanguageRegistry, Point, }; use parking_lot::Mutex; +use project::project_settings::{LspSettings, ProjectSettings}; use project::FakeFs; +use std::sync::atomic; +use std::sync::atomic::AtomicUsize; use std::{cell::RefCell, future::Future, rc::Rc, time::Instant}; use unindent::Unindent; use util::{ @@ -1796,7 +1799,7 @@ async fn test_newline_comments(cx: &mut gpui::TestAppContext) { "}); } // Ensure that comment continuations can be disabled. - update_test_settings(cx, |settings| { + update_test_language_settings(cx, |settings| { settings.defaults.extend_comment_on_newline = Some(false); }); let mut cx = EditorTestContext::new(cx).await; @@ -4546,7 +4549,7 @@ async fn test_document_format_during_save(cx: &mut gpui::TestAppContext) { assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set rust language override and assert overridden tabsize is sent to language server - update_test_settings(cx, |settings| { + update_test_language_settings(cx, |settings| { settings.languages.insert( "Rust".into(), LanguageSettingsContent { @@ -4660,7 +4663,7 @@ async fn test_range_format_during_save(cx: &mut gpui::TestAppContext) { assert!(!cx.read(|cx| editor.is_dirty(cx))); // Set rust language override and assert overridden tabsize is sent to language server - update_test_settings(cx, |settings| { + update_test_language_settings(cx, |settings| { settings.languages.insert( "Rust".into(), LanguageSettingsContent { @@ -7084,6 +7087,142 @@ async fn test_on_type_formatting_not_triggered(cx: &mut gpui::TestAppContext) { }); } +#[gpui::test] +async fn test_language_server_restart_due_to_settings_change(cx: &mut gpui::TestAppContext) { + init_test(cx, |_| {}); + + let language_name: Arc = "Rust".into(); + let mut language = Language::new( + LanguageConfig { + name: Arc::clone(&language_name), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + + let server_restarts = Arc::new(AtomicUsize::new(0)); + let closure_restarts = Arc::clone(&server_restarts); + let language_server_name = "test language server"; + let mut fake_servers = language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: language_server_name, + initialization_options: Some(json!({ + "testOptionValue": true + })), + initializer: Some(Box::new(move |fake_server| { + let task_restarts = Arc::clone(&closure_restarts); + fake_server.handle_request::(move |_, _| { + task_restarts.fetch_add(1, atomic::Ordering::Release); + futures::future::ready(Ok(())) + }); + })), + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background()); + fs.insert_tree( + "/a", + json!({ + "main.rs": "fn main() { let a = 5; }", + "other.rs": "// Test file", + }), + ) + .await; + let project = Project::test(fs, ["/a".as_ref()], cx).await; + project.update(cx, |project, _| project.languages().add(Arc::new(language))); + let (_, _workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let _buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/a/main.rs", cx) + }) + .await + .unwrap(); + let _fake_server = fake_servers.next().await.unwrap(); + update_test_language_settings(cx, |language_settings| { + language_settings.languages.insert( + Arc::clone(&language_name), + LanguageSettingsContent { + tab_size: NonZeroU32::new(8), + ..Default::default() + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + "Some other server name".into(), + LspSettings { + initialization_options: Some(json!({ + "some other init value": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 0, + "Should not restart LSP server on an unrelated LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should restart LSP server on a related LSP settings change" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: Some(json!({ + "anotherInitValue": false + })), + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 1, + "Should not restart LSP server on a related LSP settings change that is the same" + ); + + update_test_project_settings(cx, |project_settings| { + project_settings.lsp.insert( + language_server_name.into(), + LspSettings { + initialization_options: None, + }, + ); + }); + cx.foreground().run_until_parked(); + assert_eq!( + server_restarts.load(atomic::Ordering::Acquire), + 2, + "Should restart LSP server on another related LSP settings change" + ); +} + fn empty_range(row: usize, column: usize) -> Range { let point = DisplayPoint::new(row as u32, column as u32); point..point @@ -7203,7 +7342,7 @@ fn handle_copilot_completion_request( }); } -pub(crate) fn update_test_settings( +pub(crate) fn update_test_language_settings( cx: &mut TestAppContext, f: impl Fn(&mut AllLanguageSettingsContent), ) { @@ -7214,6 +7353,17 @@ pub(crate) fn update_test_settings( }); } +pub(crate) fn update_test_project_settings( + cx: &mut TestAppContext, + f: impl Fn(&mut ProjectSettings), +) { + cx.update(|cx| { + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, f); + }); + }); +} + pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) { cx.foreground().forbid_parking(); @@ -7227,5 +7377,5 @@ pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsC crate::init(cx); }); - update_test_settings(cx, f); + update_test_language_settings(cx, f); } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fafbc331892ff73512f8379eae99fd13ab974b75..f0bae9533b2298d4bd63bcc995051294a548df82 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -2916,7 +2916,7 @@ mod tests { use super::*; use crate::{ display_map::{BlockDisposition, BlockProperties}, - editor_tests::{init_test, update_test_settings}, + editor_tests::{init_test, update_test_language_settings}, Editor, MultiBuffer, }; use gpui::TestAppContext; @@ -3113,7 +3113,7 @@ mod tests { let resize_step = 10.0; let mut editor_width = 200.0; while editor_width <= 1000.0 { - update_test_settings(cx, |s| { + update_test_language_settings(cx, |s| { s.defaults.tab_size = NonZeroU32::new(tab_size); s.defaults.show_whitespaces = Some(ShowWhitespaceSetting::All); s.defaults.preferred_line_length = Some(editor_width as u32); diff --git a/crates/editor/src/inlay_hint_cache.rs b/crates/editor/src/inlay_hint_cache.rs index 70fb3725046ccf134c1d280490b66b0cbe1437eb..52473f9971c60ee5af15e8cfdff6142268cb5aa9 100644 --- a/crates/editor/src/inlay_hint_cache.rs +++ b/crates/editor/src/inlay_hint_cache.rs @@ -847,7 +847,7 @@ mod tests { use text::Point; use workspace::Workspace; - use crate::editor_tests::update_test_settings; + use crate::editor_tests::update_test_language_settings; use super::*; @@ -1476,7 +1476,7 @@ mod tests { ), ] { edits_made += 1; - update_test_settings(cx, |settings| { + update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, show_type_hints: new_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), @@ -1520,7 +1520,7 @@ mod tests { edits_made += 1; let another_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Type)]); - update_test_settings(cx, |settings| { + update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: false, show_type_hints: another_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), @@ -1577,7 +1577,7 @@ mod tests { let final_allowed_hint_kinds = HashSet::from_iter([Some(InlayHintKind::Parameter)]); edits_made += 1; - update_test_settings(cx, |settings| { + update_test_language_settings(cx, |settings| { settings.defaults.inlay_hints = Some(InlayHintSettings { enabled: true, show_type_hints: final_allowed_hint_kinds.contains(&Some(InlayHintKind::Type)), @@ -2269,7 +2269,7 @@ unedited (2nd) buffer should have the same hint"); crate::init(cx); }); - update_test_settings(cx, f); + update_test_language_settings(cx, f); } async fn prepare_test_objects( diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index d186bf630d8f19d6d892b46cf6feef5d40c2e936..642f5469cdc16133ecd2d8556215dcd6b359cdf7 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -453,6 +453,7 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result, D #[cfg(any(test, feature = "test-support"))] pub struct FakeLspAdapter { pub name: &'static str, + pub initialization_options: Option, pub capabilities: lsp::ServerCapabilities, pub initializer: Option>, pub disk_based_diagnostics_progress_token: Option, @@ -1663,6 +1664,7 @@ impl Default for FakeLspAdapter { capabilities: lsp::LanguageServer::full_capabilities(), initializer: None, disk_based_diagnostics_progress_token: None, + initialization_options: None, disk_based_diagnostics_sources: Vec::new(), } } @@ -1712,6 +1714,10 @@ impl LspAdapter for Arc { async fn disk_based_diagnostics_progress_token(&self) -> Option { self.disk_based_diagnostics_progress_token.clone() } + + async fn initialization_options(&self) -> Option { + self.initialization_options.clone() + } } fn get_capture_indices(query: &Query, captures: &mut [(&str, &mut Option)]) { From 98a0113ac398495e10ecc5e6fbbc708df41bc396 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 11 Jul 2023 13:58:55 -0400 Subject: [PATCH 29/29] Add call events Co-Authored-By: Max Brunsfeld --- crates/call/src/call.rs | 43 ++++++++++++++++++- crates/client/src/telemetry.rs | 4 ++ crates/collab/src/tests/integration_tests.rs | 6 +-- .../src/tests/randomized_integration_tests.rs | 2 +- crates/collab_ui/src/collab_ui.rs | 15 ++----- .../src/incoming_call_notification.rs | 4 +- crates/editor/src/editor.rs | 4 +- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/crates/call/src/call.rs b/crates/call/src/call.rs index 0a8f150194ed201401218d29d6b1602f5c1a1d82..ed5e560218321b9bad4592700c4c6d504c2d9ab5 100644 --- a/crates/call/src/call.rs +++ b/crates/call/src/call.rs @@ -4,7 +4,7 @@ pub mod room; use std::sync::Arc; use anyhow::{anyhow, Result}; -use client::{proto, Client, TypedEnvelope, User, UserStore}; +use client::{proto, ClickhouseEvent, Client, TelemetrySettings, TypedEnvelope, User, UserStore}; use collections::HashSet; use futures::{future::Shared, FutureExt}; use postage::watch; @@ -198,6 +198,7 @@ impl ActiveCall { let result = invite.await; this.update(&mut cx, |this, cx| { this.pending_invites.remove(&called_user_id); + this.report_call_event("invite", cx); cx.notify(); }); result @@ -243,21 +244,26 @@ impl ActiveCall { }; let join = Room::join(&call, self.client.clone(), self.user_store.clone(), cx); + cx.spawn(|this, mut cx| async move { let room = join.await?; this.update(&mut cx, |this, cx| this.set_room(Some(room.clone()), cx)) .await?; + this.update(&mut cx, |this, cx| { + this.report_call_event("accept incoming", cx) + }); Ok(()) }) } - pub fn decline_incoming(&mut self) -> Result<()> { + pub fn decline_incoming(&mut self, cx: &mut ModelContext) -> Result<()> { let call = self .incoming_call .0 .borrow_mut() .take() .ok_or_else(|| anyhow!("no incoming call"))?; + self.report_call_event_for_room("decline incoming", call.room_id, cx); self.client.send(proto::DeclineCall { room_id: call.room_id, })?; @@ -266,6 +272,7 @@ impl ActiveCall { pub fn hang_up(&mut self, cx: &mut ModelContext) -> Task> { cx.notify(); + self.report_call_event("hang up", cx); if let Some((room, _)) = self.room.take() { room.update(cx, |room, cx| room.leave(cx)) } else { @@ -273,12 +280,28 @@ impl ActiveCall { } } + pub fn toggle_screen_sharing(&self, cx: &mut AppContext) { + if let Some(room) = self.room().cloned() { + let toggle_screen_sharing = room.update(cx, |room, cx| { + if room.is_screen_sharing() { + self.report_call_event("disable screen share", cx); + Task::ready(room.unshare_screen(cx)) + } else { + self.report_call_event("enable screen share", cx); + room.share_screen(cx) + } + }); + toggle_screen_sharing.detach_and_log_err(cx); + } + } + pub fn share_project( &mut self, project: ModelHandle, cx: &mut ModelContext, ) -> Task> { if let Some((room, _)) = self.room.as_ref() { + self.report_call_event("share project", cx); room.update(cx, |room, cx| room.share_project(project, cx)) } else { Task::ready(Err(anyhow!("no active call"))) @@ -291,6 +314,7 @@ impl ActiveCall { cx: &mut ModelContext, ) -> Result<()> { if let Some((room, _)) = self.room.as_ref() { + self.report_call_event("unshare project", cx); room.update(cx, |room, cx| room.unshare_project(project, cx)) } else { Err(anyhow!("no active call")) @@ -352,4 +376,19 @@ impl ActiveCall { pub fn pending_invites(&self) -> &HashSet { &self.pending_invites } + + fn report_call_event(&self, operation: &'static str, cx: &AppContext) { + if let Some(room) = self.room() { + self.report_call_event_for_room(operation, room.read(cx).id(), cx) + } + } + + fn report_call_event_for_room(&self, operation: &'static str, room_id: u64, cx: &AppContext) { + let telemetry = self.client.telemetry(); + let telemetry_settings = *settings::get::(cx); + + let event = ClickhouseEvent::Call { operation, room_id }; + + telemetry.report_clickhouse_event(event, telemetry_settings); + } } diff --git a/crates/client/src/telemetry.rs b/crates/client/src/telemetry.rs index 9c4e187dbc9fe8bc14bd6df8d0bf48850d238b12..959f4cc7835bcd159b00d05940b8b6cff32db3c8 100644 --- a/crates/client/src/telemetry.rs +++ b/crates/client/src/telemetry.rs @@ -70,6 +70,10 @@ pub enum ClickhouseEvent { suggestion_accepted: bool, file_extension: Option, }, + Call { + operation: &'static str, + room_id: u64, + }, } #[cfg(debug_assertions)] diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 66dc19d6906595583400615faf6516f791591b13..c32129818fdd7f7c5274b6e0fdddbd16da4b8c02 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/crates/collab/src/tests/integration_tests.rs @@ -157,7 +157,7 @@ async fn test_basic_calls( // User C receives the call, but declines it. let call_c = incoming_call_c.next().await.unwrap().unwrap(); assert_eq!(call_c.calling_user.github_login, "user_b"); - active_call_c.update(cx_c, |call, _| call.decline_incoming().unwrap()); + active_call_c.update(cx_c, |call, cx| call.decline_incoming(cx).unwrap()); assert!(incoming_call_c.next().await.unwrap().is_none()); deterministic.run_until_parked(); @@ -1080,7 +1080,7 @@ async fn test_calls_on_multiple_connections( // User B declines the call on one of the two connections, causing both connections // to stop ringing. - active_call_b2.update(cx_b2, |call, _| call.decline_incoming().unwrap()); + active_call_b2.update(cx_b2, |call, cx| call.decline_incoming(cx).unwrap()); deterministic.run_until_parked(); assert!(incoming_call_b1.next().await.unwrap().is_none()); assert!(incoming_call_b2.next().await.unwrap().is_none()); @@ -5945,7 +5945,7 @@ async fn test_contacts( [("user_b".to_string(), "online", "busy")] ); - active_call_b.update(cx_b, |call, _| call.decline_incoming().unwrap()); + active_call_b.update(cx_b, |call, cx| call.decline_incoming(cx).unwrap()); deterministic.run_until_parked(); assert_eq!( contacts(&client_a, cx_a), diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index f5dfe17d6f34f17790f131d7b244f954a4e96e0b..8062a12b83264f8ac4521f9071318d45fb5b0265 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -365,7 +365,7 @@ async fn apply_client_operation( } log::info!("{}: declining incoming call", client.username); - active_call.update(cx, |call, _| call.decline_incoming())?; + active_call.update(cx, |call, cx| call.decline_incoming(cx))?; } ClientOperation::LeaveCall => { diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 76f2e265711b079c00bab6ce3e2677e48d716030..3f5ca17a20e17ed81328cda62048f46cb8ee775a 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -11,7 +11,7 @@ mod sharing_status_indicator; use call::{ActiveCall, Room}; pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu}; -use gpui::{actions, AppContext, Task}; +use gpui::{actions, AppContext}; use std::sync::Arc; use util::ResultExt; use workspace::AppState; @@ -44,16 +44,9 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) { - if let Some(room) = ActiveCall::global(cx).read(cx).room().cloned() { - let toggle_screen_sharing = room.update(cx, |room, cx| { - if room.is_screen_sharing() { - Task::ready(room.unshare_screen(cx)) - } else { - room.share_screen(cx) - } - }); - toggle_screen_sharing.detach_and_log_err(cx); - } + ActiveCall::global(cx).update(cx, |call, cx| { + call.toggle_screen_sharing(cx); + }); } pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) { diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 12fad467e34c7f0db7fc707a4059a37990aff8e9..4066b5b229a536da0b49e0ad8c5b7ab492968e18 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -99,8 +99,8 @@ impl IncomingCallNotification { }) .detach_and_log_err(cx); } else { - active_call.update(cx, |active_call, _| { - active_call.decline_incoming().log_err(); + active_call.update(cx, |active_call, cx| { + active_call.decline_incoming(cx).log_err(); }); } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 28edd2a460f6b17e41e152991fd7b67b2d48f60a..85a428d80115ed51ddc8985aa01c9cb1c746ae25 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7565,7 +7565,7 @@ impl Editor { fn report_editor_event( &self, - name: &'static str, + operation: &'static str, file_extension: Option, cx: &AppContext, ) { @@ -7602,7 +7602,7 @@ impl Editor { let event = ClickhouseEvent::Editor { file_extension, vim_mode, - operation: name, + operation, copilot_enabled, copilot_enabled_for_language, };