From 2678dfdc57208737bf712793ee489b770aae440d Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 29 Jun 2023 17:32:04 -0400 Subject: [PATCH 01/39] 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 bdde221aca..1b754b632c 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/39] 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 933c616053..c64be95184 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/39] 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 9ca54e661a..6375c2fe4d 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 e54dcdfd1e..01da555e1e 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/39] 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 0000000000..2b657e8b37 --- /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 1b754b632c..802e4139cb 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/39] 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 afee6fcd2e..e4c489c5b5 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 64332c102a..824802630d 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 523a0af964..8f1e6172e9 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 faf69d9473..f39cd82fc1 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/39] 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 95da7ff297..bac70f139a 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 f39cd82fc1..e8084cb4be 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 9b6bf976ca..7f9a84b666 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 0000000000..0de4d84f50 --- /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/39] 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 e8084cb4be..07b095dd5e 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 0000000000..5c8d7529b9 --- /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/39] 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 afee6fcd2e..d019078834 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/39] 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 d019078834..86e5b51f19 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/39] 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 86e5b51f19..e08ce47caf 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 b6c5b7ca51..a6efbd4da1 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 3e66d6bb1c..f9ba577231 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 eae8643cf3..2bcc2254ee 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/39] 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 ba527af0bb..b3e101262d 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 15:07:12 +0200 Subject: [PATCH 12/39] chore: Replace lazy_static Mutex with const. (#2693) Mutex::new() is const-stable as of Rust 1.63. Release Notes: - N/A --- Cargo.lock | 2 -- crates/collab/src/db.rs | 5 +---- crates/collab/src/tests/randomized_integration_tests.rs | 4 ++-- crates/db/src/db.rs | 2 +- crates/language/src/syntax_map.rs | 5 +---- crates/live_kit_client/Cargo.toml | 3 --- crates/live_kit_client/src/test.rs | 7 ++----- crates/vim/Cargo.toml | 1 - crates/vim/src/test/neovim_connection.rs | 6 +----- 9 files changed, 8 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ac089cee18..60ed830683 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4004,7 +4004,6 @@ dependencies = [ "gpui", "hmac 0.12.1", "jwt", - "lazy_static", "live_kit_server", "log", "media", @@ -8398,7 +8397,6 @@ dependencies = [ "indoc", "itertools", "language", - "lazy_static", "log", "nvim-rs", "parking_lot 0.11.2", diff --git a/crates/collab/src/db.rs b/crates/collab/src/db.rs index 208da22efe..e16fa9edb1 100644 --- a/crates/collab/src/db.rs +++ b/crates/collab/src/db.rs @@ -3517,7 +3517,6 @@ pub use test::*; mod test { use super::*; use gpui::executor::Background; - use lazy_static::lazy_static; use parking_lot::Mutex; use sea_orm::ConnectionTrait; use sqlx::migrate::MigrateDatabase; @@ -3566,9 +3565,7 @@ mod test { } pub fn postgres(background: Arc) -> Self { - lazy_static! { - static ref LOCK: Mutex<()> = Mutex::new(()); - } + static LOCK: Mutex<()> = Mutex::new(()); let _guard = LOCK.lock(); let mut rng = StdRng::from_entropy(); diff --git a/crates/collab/src/tests/randomized_integration_tests.rs b/crates/collab/src/tests/randomized_integration_tests.rs index a95938f6b8..f5dfe17d6f 100644 --- a/crates/collab/src/tests/randomized_integration_tests.rs +++ b/crates/collab/src/tests/randomized_integration_tests.rs @@ -37,9 +37,9 @@ use util::ResultExt; lazy_static::lazy_static! { static ref PLAN_LOAD_PATH: Option = path_env_var("LOAD_PLAN"); static ref PLAN_SAVE_PATH: Option = path_env_var("SAVE_PLAN"); - static ref LOADED_PLAN_JSON: Mutex>> = Default::default(); - static ref PLAN: Mutex>>> = Default::default(); } +static LOADED_PLAN_JSON: Mutex>> = Mutex::new(None); +static PLAN: Mutex>>> = Mutex::new(None); #[gpui::test(iterations = 100, on_failure = "on_failure")] async fn test_random_collaboration( diff --git a/crates/db/src/db.rs b/crates/db/src/db.rs index 5b8aca07e0..798dfbc17f 100644 --- a/crates/db/src/db.rs +++ b/crates/db/src/db.rs @@ -43,10 +43,10 @@ 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()); - static ref DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(()); pub static ref BACKUP_DB_PATH: RwLock> = RwLock::new(None); pub static ref ALL_FILE_DB_FAILED: AtomicBool = AtomicBool::new(false); } +static DB_FILE_OPERATIONS: Mutex<()> = Mutex::new(()); /// Open or create a database at the given directory path. /// This will retry a couple times if there are failures. If opening fails once, the db directory diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 1570baf185..b6431c2286 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -4,7 +4,6 @@ mod syntax_map_tests; use crate::{Grammar, InjectionConfig, Language, LanguageRegistry}; use collections::HashMap; use futures::FutureExt; -use lazy_static::lazy_static; use parking_lot::Mutex; use std::{ borrow::Cow, @@ -25,9 +24,7 @@ thread_local! { static PARSER: RefCell = RefCell::new(Parser::new()); } -lazy_static! { - static ref QUERY_CURSORS: Mutex> = Default::default(); -} +static QUERY_CURSORS: Mutex> = Mutex::new(vec![]); #[derive(Default)] pub struct SyntaxMap { diff --git a/crates/live_kit_client/Cargo.toml b/crates/live_kit_client/Cargo.toml index 36087a42a3..78f435906b 100644 --- a/crates/live_kit_client/Cargo.toml +++ b/crates/live_kit_client/Cargo.toml @@ -17,7 +17,6 @@ test-support = [ "async-trait", "collections/test-support", "gpui/test-support", - "lazy_static", "live_kit_server", "nanoid", ] @@ -38,7 +37,6 @@ parking_lot.workspace = true postage.workspace = true async-trait = { workspace = true, optional = true } -lazy_static = { workspace = true, optional = true } nanoid = { version ="0.4", optional = true} [dev-dependencies] @@ -60,7 +58,6 @@ foreign-types = "0.3" futures.workspace = true hmac = "0.12" jwt = "0.16" -lazy_static.workspace = true objc = "0.2" parking_lot.workspace = true serde.workspace = true diff --git a/crates/live_kit_client/src/test.rs b/crates/live_kit_client/src/test.rs index 3fc046c5a2..ada864fc44 100644 --- a/crates/live_kit_client/src/test.rs +++ b/crates/live_kit_client/src/test.rs @@ -1,18 +1,15 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; -use collections::HashMap; +use collections::{BTreeMap, HashMap}; use futures::Stream; use gpui::executor::Background; -use lazy_static::lazy_static; use live_kit_server::token; use media::core_video::CVImageBuffer; use parking_lot::Mutex; use postage::watch; use std::{future::Future, mem, sync::Arc}; -lazy_static! { - static ref SERVERS: Mutex>> = Default::default(); -} +static SERVERS: Mutex>> = Mutex::new(BTreeMap::new()); pub struct TestServer { pub url: String, diff --git a/crates/vim/Cargo.toml b/crates/vim/Cargo.toml index 57d3821379..47a85f4ed3 100644 --- a/crates/vim/Cargo.toml +++ b/crates/vim/Cargo.toml @@ -36,7 +36,6 @@ workspace = { path = "../workspace" } [dev-dependencies] indoc.workspace = true parking_lot.workspace = true -lazy_static.workspace = true editor = { path = "../editor", features = ["test-support"] } gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/vim/src/test/neovim_connection.rs b/crates/vim/src/test/neovim_connection.rs index c3916722dd..aa14e4a065 100644 --- a/crates/vim/src/test/neovim_connection.rs +++ b/crates/vim/src/test/neovim_connection.rs @@ -11,8 +11,6 @@ use gpui::keymap_matcher::Keystroke; use language::Point; -#[cfg(feature = "neovim")] -use lazy_static::lazy_static; #[cfg(feature = "neovim")] use nvim_rs::{ create::tokio::new_child_cmd, error::LoopError, Handler, Neovim, UiAttachOptions, Value, @@ -32,9 +30,7 @@ use collections::VecDeque; // Neovim doesn't like to be started simultaneously from multiple threads. We use this lock // to ensure we are only constructing one neovim connection at a time. #[cfg(feature = "neovim")] -lazy_static! { - static ref NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(()); -} +static NEOVIM_LOCK: ReentrantMutex<()> = ReentrantMutex::new(()); #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum NeovimData { From d69b07bafd1f20c3415172143fabfee66965be70 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Fri, 7 Jul 2023 16:30:19 +0200 Subject: [PATCH 13/39] 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 73450e7c7d..606a77cd2f 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 14/39] 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 bdf677512c..2f742814a8 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 3da8c24617..5eed301367 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 0000000000..c1358f7a33 --- /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 0df16f4bab..09bdbf65be 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 15/39] 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 798dfbc17f..7b4aa74a80 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 c1358f7a33..0450b5908b 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 16/39] 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 0450b5908b..a8c4b30816 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 17/39] 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 4ccab09cbe..71909ae177 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 18/39] 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 73450e7c7d..ed3315ab5a 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 19/39] 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 135194df6a..ebd504d02c 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 20/39] 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 606a77cd2f..3d4d314902 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 21/39] 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 0000000000..f37f6a2087 --- /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 0000000000..ec78f152e1 --- /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 802e4139cb..3205268f53 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 22/39] 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 2b657e8b37..0c43e7010e 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 23/39] 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 adec3dee62..cfc1f8d813 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 24/39] 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 60ed830683..ba24a756d1 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 1708ccfc0a..5757871962 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 6fc06198fe..8c3a1f407c 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 591d6e443f..1f28c05158 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 f81885c07a..4a38c2691c 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 57e3ea711d..6cfc9d8e30 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 26d9c70a43..76f2e26571 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 0000000000..4ddf1214d0 --- /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 16fefbd2eb..b5b1036b36 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 25/39] 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 e96f1efe92..bd662c039b 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 26/39] 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 8d7b8ffad6..98fb887ffd 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 e96f1efe92..e79110c81e 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 74b8e0ddb6..431ccf0bfe 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 27/39] 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 98fb887ffd..28edd2a460 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 e79110c81e..c07d18767c 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 f164eb5289109699077bcef7d99bce9dac005c30 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:23:17 +0200 Subject: [PATCH 28/39] recent_projects: Perform fuzzy search on compacted paths. Match highlighting for recent projects picker was off, because the path representation was compacted - for a path '/Users/hiro/Projects/zed' we compact it to use a tilde instead of home directory. However, the highlight positions were always calculated for a full path, leading to a mismatch in highlights. This commit addresses this by running fuzzy search on compacted paths instead of using long paths. This might lead to a slight performance hit, but given that recent projects modal shouldn't have that many items in the first place, it should be okay. Z-2546 --- crates/recent_projects/src/recent_projects.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 4ba6103167..f04dab9edc 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -134,7 +134,10 @@ impl PickerDelegate for RecentProjectsDelegate { let combined_string = location .paths() .iter() - .map(|path| path.to_string_lossy().to_owned()) + .map(|path| { + let compact = util::paths::compact(&path); + compact.to_string_lossy().into_owned() + }) .collect::>() .join(""); StringMatchCandidate::new(id, combined_string) From 15010e94fda315d5743f0e572563fc2357a18de2 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Tue, 11 Jul 2023 15:29:15 +0200 Subject: [PATCH 29/39] fixup! recent_projects: Perform fuzzy search on compacted paths. --- crates/recent_projects/src/recent_projects.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index f04dab9edc..cd512f1e57 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -134,10 +134,7 @@ impl PickerDelegate for RecentProjectsDelegate { let combined_string = location .paths() .iter() - .map(|path| { - let compact = util::paths::compact(&path); - compact.to_string_lossy().into_owned() - }) + .map(|path| util::paths::compact(&path).to_string_lossy().into_owned()) .collect::>() .join(""); StringMatchCandidate::new(id, combined_string) From 91832c8cd8de4743a5c8dad87005a67d9601d7e5 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 11 Jul 2023 13:20:02 +0300 Subject: [PATCH 30/39] 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 e8450344b8..d186bf630d 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 81db0c7ed7..dc4c8852dd 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 31/39] 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 9e726d6cc4..7b36287dca 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 fafbc33189..f0bae9533b 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 70fb372504..52473f9971 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 d186bf630d..642f5469cd 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 efe8b8b6d0ddeed8bba92a38e04f8b7f32e387a9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 11 Jul 2023 20:46:45 +0300 Subject: [PATCH 32/39] Revert "Fix language servers improper restarts" This reverts commit 91832c8cd8de4743a5c8dad87005a67d9601d7e5. --- crates/language/src/language.rs | 28 +--------------------------- crates/project/src/project.rs | 17 ++++++++++++----- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 642f5469cd..976d8062ea 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -90,8 +90,7 @@ pub struct LanguageServerName(pub Arc); /// once at startup, and caches the results. pub struct CachedLspAdapter { pub name: LanguageServerName, - initialization_options: Option, - initialization_overrides: Mutex>, + pub initialization_options: Option, pub disk_based_diagnostic_sources: Vec, pub disk_based_diagnostics_progress_token: Option, pub language_ids: HashMap, @@ -110,7 +109,6 @@ impl CachedLspAdapter { Arc::new(CachedLspAdapter { name, initialization_options, - initialization_overrides: Mutex::new(None), disk_based_diagnostic_sources, disk_based_diagnostics_progress_token, language_ids, @@ -210,30 +208,6 @@ 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 dc4c8852dd..81db0c7ed7 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, paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, - TryFutureExt as _, + debug_panic, defer, http::HttpClient, merge_json_value_into, + 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.update_initialization_overrides(new_lsp_settings) { + if adapter.initialization_options.as_ref() != new_lsp_settings { language_servers_to_restart.push((worktree, Arc::clone(language))); } } @@ -2545,13 +2545,20 @@ 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(); - adapter.update_initialization_overrides(override_options.as_ref()); + + 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, + _ => {} + } 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 98a0113ac398495e10ecc5e6fbbc708df41bc396 Mon Sep 17 00:00:00 2001 From: "Joseph T. Lyons" Date: Tue, 11 Jul 2023 13:58:55 -0400 Subject: [PATCH 33/39] 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 0a8f150194..ed5e560218 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 9c4e187dbc..959f4cc783 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 66dc19d690..c32129818f 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 f5dfe17d6f..8062a12b83 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 76f2e26571..3f5ca17a20 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 12fad467e3..4066b5b229 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 28edd2a460..85a428d801 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, }; From 4b4d049b0a7b5ce278051202c82dec09c898ae50 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Tue, 11 Jul 2023 21:29:47 +0300 Subject: [PATCH 34/39] Refactor LSP restart logic Instead of storing `initialization_options` in every LSP adapter as before, store previous LSP settings in `Project` entirely. This way, we can later have use multiple different project configurations per single LSP with its associated adapter. co-authored-by: Max Brunsfeld --- crates/command_palette/src/command_palette.rs | 1 + crates/project/src/project.rs | 30 ++++++++++++++----- crates/terminal_view/src/terminal_view.rs | 1 + crates/workspace/src/pane.rs | 1 + 4 files changed, 25 insertions(+), 8 deletions(-) diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index aec876bd78..77dde09875 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/crates/command_palette/src/command_palette.rs @@ -369,6 +369,7 @@ mod tests { editor::init(cx); workspace::init(app_state.clone(), cx); init(cx); + Project::init_settings(cx); app_state }) } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 81db0c7ed7..364b19e3a9 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -50,7 +50,7 @@ use lsp::{ }; use lsp_command::*; use postage::watch; -use project_settings::ProjectSettings; +use project_settings::{LspSettings, ProjectSettings}; use rand::prelude::*; use search::SearchQuery; use serde::Serialize; @@ -149,6 +149,7 @@ pub struct Project { _maintain_workspace_config: Task<()>, terminals: Terminals, copilot_enabled: bool, + current_lsp_settings: HashMap, LspSettings>, } struct DelayedDebounced { @@ -614,6 +615,7 @@ impl Project { local_handles: Vec::new(), }, copilot_enabled: Copilot::global(cx).is_some(), + current_lsp_settings: settings::get::(cx).lsp.clone(), } }) } @@ -706,6 +708,7 @@ impl Project { local_handles: Vec::new(), }, copilot_enabled: Copilot::global(cx).is_some(), + current_lsp_settings: settings::get::(cx).lsp.clone(), }; for worktree in worktrees { let _ = this.add_worktree(&worktree, cx); @@ -779,7 +782,9 @@ impl Project { let mut language_servers_to_stop = Vec::new(); let mut language_servers_to_restart = Vec::new(); let languages = self.languages.to_vec(); - let project_settings = settings::get::(cx).clone(); + + let new_lsp_settings = settings::get::(cx).lsp.clone(); + let current_lsp_settings = &self.current_lsp_settings; for (worktree_id, started_lsp_name) in self.language_server_ids.keys() { let language = languages.iter().find_map(|l| { let adapter = l @@ -796,16 +801,25 @@ impl Project { if !language_settings(Some(language), file.as_ref(), cx).enable_language_server { language_servers_to_stop.push((*worktree_id, started_lsp_name.clone())); } else if let Some(worktree) = worktree { - let new_lsp_settings = project_settings - .lsp - .get(&adapter.name.0) - .and_then(|s| s.initialization_options.as_ref()); - if adapter.initialization_options.as_ref() != new_lsp_settings { - language_servers_to_restart.push((worktree, Arc::clone(language))); + let server_name = &adapter.name.0; + match ( + current_lsp_settings.get(server_name), + new_lsp_settings.get(server_name), + ) { + (None, None) => {} + (Some(_), None) | (None, Some(_)) => { + language_servers_to_restart.push((worktree, Arc::clone(language))); + } + (Some(current_lsp_settings), Some(new_lsp_settings)) => { + if current_lsp_settings != new_lsp_settings { + language_servers_to_restart.push((worktree, Arc::clone(language))); + } + } } } } } + self.current_lsp_settings = new_lsp_settings; // Stop all newly-disabled language servers. for (worktree_id, adapter_name) in language_servers_to_stop { diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index c40a1a7ccd..f7963f6e5f 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -907,6 +907,7 @@ mod tests { let params = cx.update(AppState::test); cx.update(|cx| { theme::init((), cx); + Project::init_settings(cx); language::init(cx); }); diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 6a20fab9a2..8e6e107488 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2316,6 +2316,7 @@ mod tests { cx.set_global(SettingsStore::test(cx)); theme::init((), cx); crate::init_settings(cx); + Project::init_settings(cx); }); } From be881369fafce53fd43cdcf2e67f4b9966db37d4 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 11 Jul 2023 12:12:37 -0700 Subject: [PATCH 35/39] Fix a bug where the terminal panel's items wouldn't be hooked up properly to workspace actions --- crates/terminal_view/src/terminal_panel.rs | 10 ++++++++++ crates/terminal_view/src/terminal_view.rs | 2 +- crates/workspace/src/item.rs | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 11f8f7abde..2670226e26 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -221,6 +221,16 @@ impl TerminalPanel { pane::Event::ZoomIn => cx.emit(Event::ZoomIn), pane::Event::ZoomOut => cx.emit(Event::ZoomOut), pane::Event::Focus => cx.emit(Event::Focus), + + pane::Event::AddItem { item } => { + if let Some(workspace) = self.workspace.upgrade(cx) { + let pane = self.pane.clone(); + workspace.update(cx, |workspace, cx| { + item.added_to_pane(workspace,pane, cx) + }) + } + }, + _ => {} } } diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index c40a1a7ccd..d1219d53e0 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -275,7 +275,7 @@ impl TerminalView { cx.spawn(|this, mut cx| async move { Timer::after(CURSOR_BLINK_INTERVAL).await; this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx)) - .log_err(); + .ok(); }) .detach(); } diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index a3e3ab9299..0c7a478e31 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -27,7 +27,7 @@ use std::{ }; use theme::Theme; -#[derive(Eq, PartialEq, Hash)] +#[derive(Eq, PartialEq, Hash, Debug)] pub enum ItemEvent { CloseItem, UpdateTab, From 550aa2d6bdc71123b6352355a42055ad265906fd Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 11 Jul 2023 12:17:50 -0700 Subject: [PATCH 36/39] fmt --- crates/terminal_view/src/terminal_panel.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs index 2670226e26..ad61903a9d 100644 --- a/crates/terminal_view/src/terminal_panel.rs +++ b/crates/terminal_view/src/terminal_panel.rs @@ -225,11 +225,9 @@ impl TerminalPanel { pane::Event::AddItem { item } => { if let Some(workspace) = self.workspace.upgrade(cx) { let pane = self.pane.clone(); - workspace.update(cx, |workspace, cx| { - item.added_to_pane(workspace,pane, cx) - }) + workspace.update(cx, |workspace, cx| item.added_to_pane(workspace, pane, cx)) } - }, + } _ => {} } From ef296e46cbbcfeacac8f194ab1a343fd984c4729 Mon Sep 17 00:00:00 2001 From: Julia Date: Tue, 11 Jul 2023 16:49:53 -0400 Subject: [PATCH 37/39] Avoid user menu toggle button overlapping with tab bar top border --- styles/src/style_tree/titlebar.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/styles/src/style_tree/titlebar.ts b/styles/src/style_tree/titlebar.ts index 60894b08f6..177a8c5bd8 100644 --- a/styles/src/style_tree/titlebar.ts +++ b/styles/src/style_tree/titlebar.ts @@ -84,7 +84,7 @@ function user_menu() { base: { corner_radius: 6, height: button_height, - width: online ? 37 : 24, + width: 20, padding: { top: 2, bottom: 2, @@ -153,6 +153,7 @@ function user_menu() { }, } } + return { user_menu_button_online: build_button({ online: true }), user_menu_button_offline: build_button({ online: false }), From 5086e37e73bf93b65a2da784f72559d9b7dde967 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 12 Jul 2023 13:27:14 +0200 Subject: [PATCH 38/39] chore: Bump ipc-channel to 0.16.1. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Kevin Hovsäter reported a crash in cli when running 'cargo run -po cli -- --bundle-path target/debug/Zed'. It was caused by unaligned pointer access in ipc-channel library; rustc started generating debug_asserts for pointer alignment starting with 1.70, which we have oh-so-conveniently upgraded to shortly before Kevin noticed a fix. Rust 1.70 did not introduce this panic, it merely started triggering on UB that was previously ignored. --- Cargo.lock | 62 ++++++++++++------------------------------------------ 1 file changed, 13 insertions(+), 49 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60ed830683..e2af61b810 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -482,7 +482,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "crossbeam-utils 0.8.15", + "crossbeam-utils", "futures-channel", "futures-core", "futures-io", @@ -1550,7 +1550,7 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62ec6771ecfa0762d24683ee5a32ad78487a3d3afdc0fb8cae19d2c5deb50b7c" dependencies = [ - "crossbeam-utils 0.8.15", + "crossbeam-utils", ] [[package]] @@ -1863,16 +1863,6 @@ dependencies = [ "cfg-if 1.0.0", ] -[[package]] -name = "crossbeam-channel" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" -dependencies = [ - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - [[package]] name = "crossbeam-channel" version = "0.5.8" @@ -1880,7 +1870,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.15", + "crossbeam-utils", ] [[package]] @@ -1891,7 +1881,7 @@ checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" dependencies = [ "cfg-if 1.0.0", "crossbeam-epoch", - "crossbeam-utils 0.8.15", + "crossbeam-utils", ] [[package]] @@ -1902,7 +1892,7 @@ checksum = "46bd5f3f85273295a9d14aedfb86f6aadbff6d8f5295c4a9edb08e819dcf5695" dependencies = [ "autocfg 1.1.0", "cfg-if 1.0.0", - "crossbeam-utils 0.8.15", + "crossbeam-utils", "memoffset 0.8.0", "scopeguard", ] @@ -1914,18 +1904,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.15", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" -dependencies = [ - "autocfg 1.1.0", - "cfg-if 0.1.10", - "lazy_static", + "crossbeam-utils", ] [[package]] @@ -3521,12 +3500,12 @@ dependencies = [ [[package]] name = "ipc-channel" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb1d9211085f0ea6f1379d944b93c4d07e8207aa3bcf49f37eda12b85081887" +checksum = "342d636452fbc2895574e0b319b23c014fd01c9ed71dcd87f6a4a8e2f948db4b" dependencies = [ "bincode", - "crossbeam-channel 0.4.4", + "crossbeam-channel", "fnv", "lazy_static", "libc", @@ -3534,7 +3513,7 @@ dependencies = [ "rand 0.7.3", "serde", "tempfile", - "uuid 0.8.2", + "uuid 1.3.2", "winapi 0.3.9", ] @@ -3576,7 +3555,7 @@ checksum = "334e04b4d781f436dc315cb1e7515bd96826426345d498149e4bde36b67f8ee9" dependencies = [ "async-channel", "castaway", - "crossbeam-utils 0.8.15", + "crossbeam-utils", "curl", "curl-sys", "encoding_rs", @@ -4148,12 +4127,6 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4facc753ae494aeb6e3c22f839b158aebd4f9270f55cd3c79906c45476c47ab4" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "md-5" version = "0.10.5" @@ -5677,9 +5650,9 @@ version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" dependencies = [ - "crossbeam-channel 0.5.8", + "crossbeam-channel", "crossbeam-deque", - "crossbeam-utils 0.8.15", + "crossbeam-utils", "num_cpus", ] @@ -8332,15 +8305,6 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc7e3b898aa6f6c08e5295b6c89258d1331e9ac578cc992fb818759951bdc22" -[[package]] -name = "uuid" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" -dependencies = [ - "getrandom 0.2.9", -] - [[package]] name = "uuid" version = "1.3.2" From 78c83246982553451eadcb36263322993b4a3f86 Mon Sep 17 00:00:00 2001 From: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Date: Wed, 12 Jul 2023 16:53:01 +0200 Subject: [PATCH 39/39] chore: Disable http2 feature in isahc. This removes transitive dependency on libnghttp2, which is pretty heavy. --- Cargo.lock | 11 ----------- Cargo.toml | 3 ++- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 60ed830683..4a66ea1e89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1990,7 +1990,6 @@ checksum = "14d05c10f541ae6f3bc5b3d923c20001f47db7d5f0b2bc6ad16490133842db79" dependencies = [ "cc", "libc", - "libnghttp2-sys", "libz-sys", "openssl-sys", "pkg-config", @@ -3906,16 +3905,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" -[[package]] -name = "libnghttp2-sys" -version = "0.1.7+1.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ed28aba195b38d5ff02b9170cbff627e336a20925e43b4945390401c5dc93f" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "libsqlite3-sys" version = "0.24.2" diff --git a/Cargo.toml b/Cargo.toml index 1708ccfc0a..4adebc0ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,7 +81,8 @@ env_logger = { version = "0.9" } futures = { version = "0.3" } globset = { version = "0.4" } indoc = "1" -isahc = "1.7.2" +# We explicitly disable a http2 support in isahc. +isahc = { version = "1.7.2", default-features = false, features = ["static-curl", "text-decoding"] } lazy_static = { version = "1.4.0" } log = { version = "0.4.16", features = ["kv_unstable_serde"] } ordered-float = { version = "2.1.1" }