From fc36c706d33acaeb020347f3228dc125adaf3d27 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Tue, 8 Mar 2022 15:34:44 -0800 Subject: [PATCH 01/18] Add missing mouse button events and mouse history navigation Co-Authored-By: Max Brunsfeld Co-Authored-By: Nathan Sobo --- crates/gpui/src/elements/event_handler.rs | 38 +++++++++++- crates/gpui/src/platform/event.rs | 24 +++++++ crates/gpui/src/platform/mac/event.rs | 42 +++++++++++++ crates/gpui/src/platform/mac/window.rs | 16 +++++ crates/workspace/src/pane.rs | 76 +++++++++++++++-------- crates/zed/src/zed.rs | 20 +++--- 6 files changed, 179 insertions(+), 37 deletions(-) diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 0eea82fa02..2132939c1c 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -10,6 +10,8 @@ pub struct EventHandler { child: ElementBox, capture: Option bool>>, mouse_down: Option bool>>, + right_mouse_down: Option bool>>, + other_mouse_down: Option bool>>, } impl EventHandler { @@ -18,6 +20,8 @@ impl EventHandler { child, capture: None, mouse_down: None, + right_mouse_down: None, + other_mouse_down: None, } } @@ -29,6 +33,22 @@ impl EventHandler { self } + pub fn on_right_mouse_down(mut self, callback: F) -> Self + where + F: 'static + FnMut(&mut EventContext) -> bool, + { + self.right_mouse_down = Some(Box::new(callback)); + self + } + + pub fn on_other_mouse_down(mut self, callback: F) -> Self + where + F: 'static + FnMut(u16, &mut EventContext) -> bool, + { + self.other_mouse_down = Some(Box::new(callback)); + self + } + pub fn capture(mut self, callback: F) -> Self where F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool, @@ -86,7 +106,23 @@ impl Element for EventHandler { } } false - } + }, + Event::RightMouseDown { position, .. } => { + if let Some(callback) = self.right_mouse_down.as_mut() { + if bounds.contains_point(*position) { + return callback(cx); + } + } + false + }, + Event::OtherMouseDown { position, button, .. } => { + if let Some(callback) = self.other_mouse_down.as_mut() { + if bounds.contains_point(*position) { + return callback(*button, cx); + } + } + false + }, _ => false, } } diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 98762b306a..19b8eec189 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -26,6 +26,30 @@ pub enum Event { LeftMouseDragged { position: Vector2F, }, + RightMouseDown { + position: Vector2F, + ctrl: bool, + alt: bool, + shift: bool, + cmd: bool, + click_count: usize, + }, + RightMouseUp { + position: Vector2F, + }, + OtherMouseDown { + position: Vector2F, + button: u16, + ctrl: bool, + alt: bool, + shift: bool, + cmd: bool, + click_count: usize, + }, + OtherMouseUp { + position: Vector2F, + button: u16, + }, MouseMoved { position: Vector2F, left_mouse_down: bool, diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 7c870e4720..4b1f9d3a38 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -125,6 +125,48 @@ impl Event { window_height - native_event.locationInWindow().y as f32, ), }), + NSEventType::NSRightMouseDown => { + let modifiers = native_event.modifierFlags(); + window_height.map(|window_height| Self::RightMouseDown { + position: vec2f( + native_event.locationInWindow().x as f32, + window_height - native_event.locationInWindow().y as f32, + ), + ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask), + alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), + shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask), + cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask), + click_count: native_event.clickCount() as usize, + }) + } + NSEventType::NSRightMouseUp => window_height.map(|window_height| Self::RightMouseUp { + position: vec2f( + native_event.locationInWindow().x as f32, + window_height - native_event.locationInWindow().y as f32, + ), + }), + NSEventType::NSOtherMouseDown => { + let modifiers = native_event.modifierFlags(); + window_height.map(|window_height| Self::OtherMouseDown { + position: vec2f( + native_event.locationInWindow().x as f32, + window_height - native_event.locationInWindow().y as f32, + ), + button: native_event.buttonNumber() as u16, + ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask), + alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), + shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask), + cmd: modifiers.contains(NSEventModifierFlags::NSCommandKeyMask), + click_count: native_event.clickCount() as usize, + }) + } + NSEventType::NSOtherMouseUp => window_height.map(|window_height| Self::OtherMouseUp { + position: vec2f( + native_event.locationInWindow().x as f32, + window_height - native_event.locationInWindow().y as f32, + ), + button: native_event.buttonNumber() as u16, + }), NSEventType::NSLeftMouseDragged => { window_height.map(|window_height| Self::LeftMouseDragged { position: vec2f( diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 9fd34166ff..5281009155 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -95,6 +95,22 @@ unsafe fn build_classes() { sel!(mouseUp:), handle_view_event as extern "C" fn(&Object, Sel, id), ); + decl.add_method( + sel!(rightMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(rightMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseDown:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); + decl.add_method( + sel!(otherMouseUp:), + handle_view_event as extern "C" fn(&Object, Sel, id), + ); decl.add_method( sel!(mouseMoved:), handle_view_event as extern "C" fn(&Object, Sel, id), diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 78f84d7da2..02a7c03c90 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -8,7 +8,7 @@ use gpui::{ keymap::Binding, platform::CursorStyle, AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, - ViewHandle, + ViewHandle, WeakViewHandle, }; use postage::watch; use project::ProjectPath; @@ -27,8 +27,8 @@ action!(ActivateNextItem); action!(CloseActiveItem); action!(CloseInactiveItems); action!(CloseItem, usize); -action!(GoBack); -action!(GoForward); +action!(GoBack, Option>); +action!(GoForward, Option>); const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; @@ -54,11 +54,19 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|pane: &mut Pane, action: &Split, cx| { pane.split(action.0, cx); }); - cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { - Pane::go_back(workspace, cx).detach(); + cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| { + Pane::go_back( + workspace, + action.0.as_ref().and_then(|weak_handle| weak_handle.upgrade(cx)), + cx + ).detach(); }); - cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { - Pane::go_forward(workspace, cx).detach(); + cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| { + Pane::go_forward( + workspace, + action.0.as_ref().and_then(|weak_handle| weak_handle.upgrade(cx)), + cx + ).detach(); }); cx.add_bindings(vec![ @@ -70,8 +78,8 @@ pub fn init(cx: &mut MutableAppContext) { Binding::new("cmd-k down", Split(SplitDirection::Down), Some("Pane")), Binding::new("cmd-k left", Split(SplitDirection::Left), Some("Pane")), Binding::new("cmd-k right", Split(SplitDirection::Right), Some("Pane")), - Binding::new("ctrl--", GoBack, Some("Pane")), - Binding::new("shift-ctrl-_", GoForward, Some("Pane")), + Binding::new("ctrl--", GoBack(None), Some("Pane")), + Binding::new("shift-ctrl-_", GoForward(None), Some("Pane")), ]); } @@ -163,19 +171,19 @@ impl Pane { cx.emit(Event::Activate); } - pub fn go_back(workspace: &mut Workspace, cx: &mut ViewContext) -> Task<()> { + pub fn go_back(workspace: &mut Workspace, pane: Option>, cx: &mut ViewContext) -> Task<()> { Self::navigate_history( workspace, - workspace.active_pane().clone(), + pane.unwrap_or_else(|| workspace.active_pane().clone()), NavigationMode::GoingBack, cx, ) } - pub fn go_forward(workspace: &mut Workspace, cx: &mut ViewContext) -> Task<()> { + pub fn go_forward(workspace: &mut Workspace, pane: Option>, cx: &mut ViewContext) -> Task<()> { Self::navigate_history( workspace, - workspace.active_pane().clone(), + pane.unwrap_or_else(|| workspace.active_pane().clone()), NavigationMode::GoingForward, cx, ) @@ -187,6 +195,8 @@ impl Pane { mode: NavigationMode, cx: &mut ViewContext, ) -> Task<()> { + workspace.activate_pane(pane.clone(), cx); + let to_load = pane.update(cx, |pane, cx| { // Retrieve the weak item handle from the history. let entry = pane.nav_history.borrow_mut().pop(mode)?; @@ -634,19 +644,33 @@ impl View for Pane { } fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - if let Some(active_item) = self.active_item() { - Flex::column() - .with_child(self.render_tabs(cx)) - .with_children( - self.active_toolbar() - .as_ref() - .map(|view| ChildView::new(view).boxed()), - ) - .with_child(ChildView::new(active_item).flexible(1., true).boxed()) - .named("pane") - } else { - Empty::new().named("pane") - } + let this = cx.handle(); + + EventHandler::new( + if let Some(active_item) = self.active_item() { + Flex::column() + .with_child(self.render_tabs(cx)) + .with_children( + self.active_toolbar() + .as_ref() + .map(|view| ChildView::new(view).boxed()), + ) + .with_child(ChildView::new(active_item).flexible(1., true).boxed()) + .boxed() + } else { + Empty::new().boxed() + } + ) + .on_other_mouse_down(move |button, cx| { + match button { + 3 => cx.dispatch_action(GoBack(Some(this.clone()))), + 4 => cx.dispatch_action(GoForward(Some(this.clone()))), + _ => return false, + }; + true + }) + .named("pane") + } fn on_focus(&mut self, cx: &mut ViewContext) { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c9a14bf236..75e7735f24 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -747,44 +747,44 @@ mod tests { (file3.clone(), DisplayPoint::new(15, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(0, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file2.clone(), DisplayPoint::new(0, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(10, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(0, 0)) ); // Go back one more time and ensure we don't navigate past the first item in the history. - workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(0, 0)) ); - workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(10, 0)) ); - workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file2.clone(), DisplayPoint::new(0, 0)) @@ -798,7 +798,7 @@ mod tests { .update(cx, |pane, cx| pane.close_item(editor3.id(), cx)); drop(editor3); }); - workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(0, 0)) @@ -818,12 +818,12 @@ mod tests { }) .await .unwrap(); - workspace.update(cx, |w, cx| Pane::go_back(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(10, 0)) ); - workspace.update(cx, |w, cx| Pane::go_forward(w, cx)).await; + workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(0, 0)) From 6ee0cceb14337c48495c56c62e50508bf1febfd2 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Wed, 9 Mar 2022 15:04:04 -0800 Subject: [PATCH 02/18] Switch to using mouse navigation events instead of other in order to get rid of opaque button id --- crates/gpui/src/elements/event_handler.rs | 28 +++++---- crates/gpui/src/gpui.rs | 2 +- crates/gpui/src/platform.rs | 2 +- crates/gpui/src/platform/event.rs | 14 +++-- crates/gpui/src/platform/mac/event.rs | 40 +++++++++--- crates/workspace/src/pane.rs | 74 ++++++++++++++--------- crates/zed/src/zed.rs | 40 +++++++++--- 7 files changed, 132 insertions(+), 68 deletions(-) diff --git a/crates/gpui/src/elements/event_handler.rs b/crates/gpui/src/elements/event_handler.rs index 2132939c1c..469bebee1a 100644 --- a/crates/gpui/src/elements/event_handler.rs +++ b/crates/gpui/src/elements/event_handler.rs @@ -3,7 +3,7 @@ use serde_json::json; use crate::{ geometry::vector::Vector2F, DebugContext, Element, ElementBox, Event, EventContext, - LayoutContext, PaintContext, SizeConstraint, + LayoutContext, NavigationDirection, PaintContext, SizeConstraint, }; pub struct EventHandler { @@ -11,7 +11,7 @@ pub struct EventHandler { capture: Option bool>>, mouse_down: Option bool>>, right_mouse_down: Option bool>>, - other_mouse_down: Option bool>>, + navigate_mouse_down: Option bool>>, } impl EventHandler { @@ -21,7 +21,7 @@ impl EventHandler { capture: None, mouse_down: None, right_mouse_down: None, - other_mouse_down: None, + navigate_mouse_down: None, } } @@ -41,11 +41,11 @@ impl EventHandler { self } - pub fn on_other_mouse_down(mut self, callback: F) -> Self + pub fn on_navigate_mouse_down(mut self, callback: F) -> Self where - F: 'static + FnMut(u16, &mut EventContext) -> bool, + F: 'static + FnMut(NavigationDirection, &mut EventContext) -> bool, { - self.other_mouse_down = Some(Box::new(callback)); + self.navigate_mouse_down = Some(Box::new(callback)); self } @@ -106,7 +106,7 @@ impl Element for EventHandler { } } false - }, + } Event::RightMouseDown { position, .. } => { if let Some(callback) = self.right_mouse_down.as_mut() { if bounds.contains_point(*position) { @@ -114,15 +114,19 @@ impl Element for EventHandler { } } false - }, - Event::OtherMouseDown { position, button, .. } => { - if let Some(callback) = self.other_mouse_down.as_mut() { + } + Event::NavigateMouseDown { + position, + direction, + .. + } => { + if let Some(callback) = self.navigate_mouse_down.as_mut() { if bounds.contains_point(*position) { - return callback(*button, cx); + return callback(*direction, cx); } } false - }, + } _ => false, } } diff --git a/crates/gpui/src/gpui.rs b/crates/gpui/src/gpui.rs index 49fc74d47b..9803a2aa2f 100644 --- a/crates/gpui/src/gpui.rs +++ b/crates/gpui/src/gpui.rs @@ -29,7 +29,7 @@ pub mod keymap; pub mod platform; pub use gpui_macros::test; pub use platform::FontSystem; -pub use platform::{Event, PathPromptOptions, Platform, PromptLevel}; +pub use platform::{Event, NavigationDirection, PathPromptOptions, Platform, PromptLevel}; pub use presenter::{ Axis, DebugContext, EventContext, LayoutContext, PaintContext, SizeConstraint, Vector2FExt, }; diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index c04145294c..66bd44b26f 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -19,7 +19,7 @@ use crate::{ }; use anyhow::Result; use async_task::Runnable; -pub use event::Event; +pub use event::{Event, NavigationDirection}; use postage::oneshot; use std::{ any::Any, diff --git a/crates/gpui/src/platform/event.rs b/crates/gpui/src/platform/event.rs index 19b8eec189..72e1c24d6b 100644 --- a/crates/gpui/src/platform/event.rs +++ b/crates/gpui/src/platform/event.rs @@ -1,5 +1,11 @@ use crate::{geometry::vector::Vector2F, keymap::Keystroke}; +#[derive(Copy, Clone, Debug)] +pub enum NavigationDirection { + Back, + Forward, +} + #[derive(Clone, Debug)] pub enum Event { KeyDown { @@ -37,18 +43,18 @@ pub enum Event { RightMouseUp { position: Vector2F, }, - OtherMouseDown { + NavigateMouseDown { position: Vector2F, - button: u16, + direction: NavigationDirection, ctrl: bool, alt: bool, shift: bool, cmd: bool, click_count: usize, }, - OtherMouseUp { + NavigateMouseUp { position: Vector2F, - button: u16, + direction: NavigationDirection, }, MouseMoved { position: Vector2F, diff --git a/crates/gpui/src/platform/mac/event.rs b/crates/gpui/src/platform/mac/event.rs index 4b1f9d3a38..33f9f22e11 100644 --- a/crates/gpui/src/platform/mac/event.rs +++ b/crates/gpui/src/platform/mac/event.rs @@ -1,4 +1,8 @@ -use crate::{geometry::vector::vec2f, keymap::Keystroke, platform::Event}; +use crate::{ + geometry::vector::vec2f, + keymap::Keystroke, + platform::{Event, NavigationDirection}, +}; use cocoa::{ appkit::{NSEvent, NSEventModifierFlags, NSEventType}, base::{id, nil, YES}, @@ -146,13 +150,20 @@ impl Event { ), }), NSEventType::NSOtherMouseDown => { + let direction = match native_event.buttonNumber() { + 3 => NavigationDirection::Back, + 4 => NavigationDirection::Forward, + // Other mouse buttons aren't tracked currently + _ => return None, + }; + let modifiers = native_event.modifierFlags(); - window_height.map(|window_height| Self::OtherMouseDown { + window_height.map(|window_height| Self::NavigateMouseDown { position: vec2f( native_event.locationInWindow().x as f32, window_height - native_event.locationInWindow().y as f32, ), - button: native_event.buttonNumber() as u16, + direction, ctrl: modifiers.contains(NSEventModifierFlags::NSControlKeyMask), alt: modifiers.contains(NSEventModifierFlags::NSAlternateKeyMask), shift: modifiers.contains(NSEventModifierFlags::NSShiftKeyMask), @@ -160,13 +171,22 @@ impl Event { click_count: native_event.clickCount() as usize, }) } - NSEventType::NSOtherMouseUp => window_height.map(|window_height| Self::OtherMouseUp { - position: vec2f( - native_event.locationInWindow().x as f32, - window_height - native_event.locationInWindow().y as f32, - ), - button: native_event.buttonNumber() as u16, - }), + NSEventType::NSOtherMouseUp => { + let direction = match native_event.buttonNumber() { + 3 => NavigationDirection::Back, + 4 => NavigationDirection::Forward, + // Other mouse buttons aren't tracked currently + _ => return None, + }; + + window_height.map(|window_height| Self::NavigateMouseUp { + position: vec2f( + native_event.locationInWindow().x as f32, + window_height - native_event.locationInWindow().y as f32, + ), + direction, + }) + } NSEventType::NSLeftMouseDragged => { window_height.map(|window_height| Self::LeftMouseDragged { position: vec2f( diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index 02a7c03c90..15195f2434 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -6,7 +6,7 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, - platform::CursorStyle, + platform::{CursorStyle, NavigationDirection}, AnyViewHandle, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; @@ -57,16 +57,24 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action(|workspace: &mut Workspace, action: &GoBack, cx| { Pane::go_back( workspace, - action.0.as_ref().and_then(|weak_handle| weak_handle.upgrade(cx)), - cx - ).detach(); + action + .0 + .as_ref() + .and_then(|weak_handle| weak_handle.upgrade(cx)), + cx, + ) + .detach(); }); cx.add_action(|workspace: &mut Workspace, action: &GoForward, cx| { Pane::go_forward( workspace, - action.0.as_ref().and_then(|weak_handle| weak_handle.upgrade(cx)), - cx - ).detach(); + action + .0 + .as_ref() + .and_then(|weak_handle| weak_handle.upgrade(cx)), + cx, + ) + .detach(); }); cx.add_bindings(vec![ @@ -171,7 +179,11 @@ impl Pane { cx.emit(Event::Activate); } - pub fn go_back(workspace: &mut Workspace, pane: Option>, cx: &mut ViewContext) -> Task<()> { + pub fn go_back( + workspace: &mut Workspace, + pane: Option>, + cx: &mut ViewContext, + ) -> Task<()> { Self::navigate_history( workspace, pane.unwrap_or_else(|| workspace.active_pane().clone()), @@ -180,7 +192,11 @@ impl Pane { ) } - pub fn go_forward(workspace: &mut Workspace, pane: Option>, cx: &mut ViewContext) -> Task<()> { + pub fn go_forward( + workspace: &mut Workspace, + pane: Option>, + cx: &mut ViewContext, + ) -> Task<()> { Self::navigate_history( workspace, pane.unwrap_or_else(|| workspace.active_pane().clone()), @@ -646,31 +662,29 @@ impl View for Pane { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let this = cx.handle(); - EventHandler::new( - if let Some(active_item) = self.active_item() { - Flex::column() - .with_child(self.render_tabs(cx)) - .with_children( - self.active_toolbar() - .as_ref() - .map(|view| ChildView::new(view).boxed()), - ) - .with_child(ChildView::new(active_item).flexible(1., true).boxed()) - .boxed() - } else { - Empty::new().boxed() + EventHandler::new(if let Some(active_item) = self.active_item() { + Flex::column() + .with_child(self.render_tabs(cx)) + .with_children( + self.active_toolbar() + .as_ref() + .map(|view| ChildView::new(view).boxed()), + ) + .with_child(ChildView::new(active_item).flexible(1., true).boxed()) + .boxed() + } else { + Empty::new().boxed() + }) + .on_navigate_mouse_down(move |direction, cx| { + let this = this.clone(); + match direction { + NavigationDirection::Back => cx.dispatch_action(GoBack(Some(this))), + NavigationDirection::Forward => cx.dispatch_action(GoForward(Some(this))), } - ) - .on_other_mouse_down(move |button, cx| { - match button { - 3 => cx.dispatch_action(GoBack(Some(this.clone()))), - 4 => cx.dispatch_action(GoForward(Some(this.clone()))), - _ => return false, - }; + true }) .named("pane") - } fn on_focus(&mut self, cx: &mut ViewContext) { diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 75e7735f24..9299988ffe 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -747,44 +747,58 @@ mod tests { (file3.clone(), DisplayPoint::new(15, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(0, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file2.clone(), DisplayPoint::new(0, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(10, 0)) ); - workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(0, 0)) ); // Go back one more time and ensure we don't navigate past the first item in the history. - workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(0, 0)) ); - workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_forward(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(10, 0)) ); - workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_forward(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file2.clone(), DisplayPoint::new(0, 0)) @@ -798,7 +812,9 @@ mod tests { .update(cx, |pane, cx| pane.close_item(editor3.id(), cx)); drop(editor3); }); - workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_forward(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(0, 0)) @@ -818,12 +834,16 @@ mod tests { }) .await .unwrap(); - workspace.update(cx, |w, cx| Pane::go_back(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_back(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file1.clone(), DisplayPoint::new(10, 0)) ); - workspace.update(cx, |w, cx| Pane::go_forward(w, None, cx)).await; + workspace + .update(cx, |w, cx| Pane::go_forward(w, None, cx)) + .await; assert_eq!( active_location(&workspace, cx), (file3.clone(), DisplayPoint::new(0, 0)) From 49e38e6e003c71cb402c42bfbbfb95789b9f6c91 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 9 Mar 2022 20:31:29 -0700 Subject: [PATCH 03/18] Eliminate ToFoldPoint trait Just make it a method on FoldMap --- crates/editor/src/display_map.rs | 8 ++-- crates/editor/src/display_map/fold_map.rs | 50 ++++++++++------------- crates/editor/src/display_map/tab_map.rs | 10 ++--- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 053883e938..d36f571308 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -6,7 +6,7 @@ mod wrap_map; use crate::{Anchor, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint}; use block_map::{BlockMap, BlockPoint}; use collections::{HashMap, HashSet}; -use fold_map::{FoldMap, ToFoldPoint as _}; +use fold_map::FoldMap; use gpui::{fonts::FontId, Entity, ModelContext, ModelHandle}; use language::{Point, Subscription as BufferSubscription}; use std::ops::Range; @@ -200,7 +200,7 @@ impl DisplaySnapshot { pub fn prev_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { loop { - let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Left); + let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Left); *fold_point.column_mut() = 0; point = fold_point.to_buffer_point(&self.folds_snapshot); @@ -216,7 +216,7 @@ impl DisplaySnapshot { pub fn next_line_boundary(&self, mut point: Point) -> (Point, DisplayPoint) { loop { - let mut fold_point = point.to_fold_point(&self.folds_snapshot, Bias::Right); + let mut fold_point = self.folds_snapshot.to_fold_point(point, Bias::Right); *fold_point.column_mut() = self.folds_snapshot.line_len(fold_point.row()); point = fold_point.to_buffer_point(&self.folds_snapshot); @@ -231,7 +231,7 @@ impl DisplaySnapshot { } fn point_to_display_point(&self, point: Point, bias: Bias) -> DisplayPoint { - let fold_point = point.to_fold_point(&self.folds_snapshot, bias); + let fold_point = self.folds_snapshot.to_fold_point(point, bias); let tab_point = self.tabs_snapshot.to_tab_point(fold_point); let wrap_point = self.wraps_snapshot.from_tab_point(tab_point); let block_point = self.blocks_snapshot.to_block_point(wrap_point); diff --git a/crates/editor/src/display_map/fold_map.rs b/crates/editor/src/display_map/fold_map.rs index 2866ae8f63..daafbee57b 100644 --- a/crates/editor/src/display_map/fold_map.rs +++ b/crates/editor/src/display_map/fold_map.rs @@ -12,10 +12,6 @@ use std::{ }; use sum_tree::{Bias, Cursor, FilterCursor, SumTree}; -pub trait ToFoldPoint { - fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint; -} - #[derive(Copy, Clone, Debug, Default, Eq, Ord, PartialOrd, PartialEq)] pub struct FoldPoint(pub super::Point); @@ -75,26 +71,6 @@ impl FoldPoint { } } -impl ToFoldPoint for Point { - fn to_fold_point(&self, snapshot: &FoldSnapshot, bias: Bias) -> FoldPoint { - let mut cursor = snapshot.transforms.cursor::<(Point, FoldPoint)>(); - cursor.seek(self, Bias::Right, &()); - if cursor.item().map_or(false, |t| t.is_fold()) { - if bias == Bias::Left || *self == cursor.start().0 { - cursor.start().1 - } else { - cursor.end(&()).1 - } - } else { - let overshoot = *self - cursor.start().0; - FoldPoint(cmp::min( - cursor.start().1 .0 + overshoot, - cursor.end(&()).1 .0, - )) - } - } -} - pub struct FoldMapWriter<'a>(&'a mut FoldMap); impl<'a> FoldMapWriter<'a> { @@ -554,6 +530,24 @@ impl FoldSnapshot { summary } + pub fn to_fold_point(&self, point: Point, bias: Bias) -> FoldPoint { + let mut cursor = self.transforms.cursor::<(Point, FoldPoint)>(); + cursor.seek(&point, Bias::Right, &()); + if cursor.item().map_or(false, |t| t.is_fold()) { + if bias == Bias::Left || point == cursor.start().0 { + cursor.start().1 + } else { + cursor.end(&()).1 + } + } else { + let overshoot = point - cursor.start().0; + FoldPoint(cmp::min( + cursor.start().1 .0 + overshoot, + cursor.end(&()).1 .0, + )) + } + } + pub fn len(&self) -> FoldOffset { FoldOffset(self.transforms.summary().output.bytes) } @@ -1356,7 +1350,7 @@ mod tests { let buffer_point = fold_point.to_buffer_point(&snapshot); let buffer_offset = buffer_point.to_offset(&buffer_snapshot); assert_eq!( - buffer_point.to_fold_point(&snapshot, Right), + snapshot.to_fold_point(buffer_point, Right), fold_point, "{:?} -> fold point", buffer_point, @@ -1428,10 +1422,8 @@ mod tests { } for fold_range in map.merged_fold_ranges() { - let fold_point = fold_range - .start - .to_point(&buffer_snapshot) - .to_fold_point(&snapshot, Right); + let fold_point = + snapshot.to_fold_point(fold_range.start.to_point(&buffer_snapshot), Right); assert!(snapshot.is_line_folded(fold_point.row())); } diff --git a/crates/editor/src/display_map/tab_map.rs b/crates/editor/src/display_map/tab_map.rs index e2239e7671..dc62783eb3 100644 --- a/crates/editor/src/display_map/tab_map.rs +++ b/crates/editor/src/display_map/tab_map.rs @@ -1,4 +1,4 @@ -use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot, ToFoldPoint}; +use super::fold_map::{self, FoldEdit, FoldPoint, FoldSnapshot}; use crate::MultiBufferSnapshot; use language::{rope, Chunk}; use parking_lot::Mutex; @@ -201,10 +201,6 @@ impl TabSnapshot { TabPoint::new(input.row(), expanded as u32) } - pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint { - self.to_tab_point(point.to_fold_point(&self.fold_snapshot, bias)) - } - pub fn to_fold_point(&self, output: TabPoint, bias: Bias) -> (FoldPoint, usize, usize) { let chars = self.fold_snapshot.chars_at(FoldPoint::new(output.row(), 0)); let expanded = output.column() as usize; @@ -217,6 +213,10 @@ impl TabSnapshot { ) } + pub fn from_point(&self, point: Point, bias: Bias) -> TabPoint { + self.to_tab_point(self.fold_snapshot.to_fold_point(point, bias)) + } + pub fn to_point(&self, point: TabPoint, bias: Bias) -> Point { self.to_fold_point(point, bias) .0 From 178442a4a858a93663f5870192385a75b1a6fe98 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 01:09:02 -0800 Subject: [PATCH 04/18] Add support for rendering cursors as a block and underscore --- crates/editor/src/editor.rs | 12 +++++++++- crates/editor/src/element.rs | 45 ++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 6 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 91d02b1aa0..711fee890b 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -450,6 +450,7 @@ pub struct Editor { document_highlights_task: Option>, pending_rename: Option, searchable: bool, + cursor_shape: CursorShape, } pub struct EditorSnapshot { @@ -930,6 +931,7 @@ impl Editor { document_highlights_task: Default::default(), pending_rename: Default::default(), searchable: true, + cursor_shape: Default::default(), }; this.end_selection(cx); this @@ -1021,6 +1023,14 @@ impl Editor { cx.notify(); } + pub fn set_cursor_shape( + &mut self, + cursor_shape: CursorShape + ) { + self.cursor_shape = cursor_shape; + // TODO: Do we need to notify? + } + pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); compute_scroll_position(&display_map, self.scroll_position, &self.scroll_top_anchor) @@ -5569,7 +5579,7 @@ impl View for Editor { self.display_map.update(cx, |map, cx| { map.set_font(style.text.font_id, style.text.font_size, cx) }); - EditorElement::new(self.handle.clone(), style.clone()).boxed() + EditorElement::new(self.handle.clone(), style.clone(), self.cursor_shape).boxed() } fn ui_name() -> &'static str { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index dcf716e0bb..85e34108d7 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -32,11 +32,12 @@ use std::{ pub struct EditorElement { view: WeakViewHandle, style: EditorStyle, + cursor_shape: CursorShape, } impl EditorElement { - pub fn new(view: WeakViewHandle, style: EditorStyle) -> Self { - Self { view, style } + pub fn new(view: WeakViewHandle, style: EditorStyle, cursor_shape: CursorShape) -> Self { + Self { view, style, cursor_shape } } fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor { @@ -362,13 +363,24 @@ impl EditorElement { if (start_row..end_row).contains(&cursor_position.row()) { let cursor_row_layout = &layout.line_layouts[(cursor_position.row() - start_row) as usize]; - let x = cursor_row_layout.x_for_index(cursor_position.column() as usize) - - scroll_left; + let cursor_column = cursor_position.column() as usize; + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); + let mut character_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; + // TODO: Is there a better option here for the character size + // at the end of the line? + // Default to 1/3 the line height + if character_width == 0.0 { + character_width = layout.line_height / 3.0; + } + + let x = cursor_character_x - scroll_left; let y = cursor_position.row() as f32 * layout.line_height - scroll_top; cursors.push(Cursor { color: style.cursor, + character_width, origin: content_origin + vec2f(x, y), line_height: layout.line_height, + shape: self.cursor_shape, }); } } @@ -1212,16 +1224,39 @@ impl PaintState { } } +#[derive(Copy, Clone)] +pub enum CursorShape { + Bar, + Block, + Underscore +} + +impl Default for CursorShape { + fn default() -> Self { + CursorShape::Bar + } +} + struct Cursor { origin: Vector2F, + character_width: f32, line_height: f32, color: Color, + shape: CursorShape } impl Cursor { fn paint(&self, cx: &mut PaintContext) { + let bounds = match self.shape { + CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), + CursorShape::Block => RectF::new(self.origin, vec2f(self.character_width, self.line_height)), + CursorShape::Underscore => RectF::new( + self.origin + Vector2F::new(0.0, self.line_height - 2.0), + vec2f(self.character_width, 2.0)), + }; + cx.scene.push_quad(Quad { - bounds: RectF::new(self.origin, vec2f(2.0, self.line_height)), + bounds, background: Some(self.color), border: Border::new(0., Color::black()), corner_radius: 0., From 0d42c851959214015e736396b9bf491d0725cae6 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 01:09:25 -0800 Subject: [PATCH 05/18] fix formatting --- crates/editor/src/editor.rs | 5 +---- crates/editor/src/element.rs | 26 +++++++++++++++++++------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 711fee890b..32a81f4f69 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1023,10 +1023,7 @@ impl Editor { cx.notify(); } - pub fn set_cursor_shape( - &mut self, - cursor_shape: CursorShape - ) { + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) { self.cursor_shape = cursor_shape; // TODO: Do we need to notify? } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 85e34108d7..822d3ab085 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -36,8 +36,16 @@ pub struct EditorElement { } impl EditorElement { - pub fn new(view: WeakViewHandle, style: EditorStyle, cursor_shape: CursorShape) -> Self { - Self { view, style, cursor_shape } + pub fn new( + view: WeakViewHandle, + style: EditorStyle, + cursor_shape: CursorShape, + ) -> Self { + Self { + view, + style, + cursor_shape, + } } fn view<'a>(&self, cx: &'a AppContext) -> &'a Editor { @@ -365,7 +373,8 @@ impl EditorElement { &layout.line_layouts[(cursor_position.row() - start_row) as usize]; let cursor_column = cursor_position.column() as usize; let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); - let mut character_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; + let mut character_width = + cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; // TODO: Is there a better option here for the character size // at the end of the line? // Default to 1/3 the line height @@ -1228,7 +1237,7 @@ impl PaintState { pub enum CursorShape { Bar, Block, - Underscore + Underscore, } impl Default for CursorShape { @@ -1242,17 +1251,20 @@ struct Cursor { character_width: f32, line_height: f32, color: Color, - shape: CursorShape + shape: CursorShape, } impl Cursor { fn paint(&self, cx: &mut PaintContext) { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), - CursorShape::Block => RectF::new(self.origin, vec2f(self.character_width, self.line_height)), + CursorShape::Block => { + RectF::new(self.origin, vec2f(self.character_width, self.line_height)) + } CursorShape::Underscore => RectF::new( self.origin + Vector2F::new(0.0, self.line_height - 2.0), - vec2f(self.character_width, 2.0)), + vec2f(self.character_width, 2.0), + ), }; cx.scene.push_quad(Quad { From ee6d7fc6d56f6cce9570d0394c96aba0c06ea469 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2022 10:05:00 +0100 Subject: [PATCH 06/18] Delete till previous tabstop when backspacing within indent column --- crates/editor/src/editor.rs | 55 +++++++++++++++++++++++-------- crates/editor/src/multi_buffer.rs | 2 +- crates/language/src/buffer.rs | 34 ++++++++++++++----- 3 files changed, 68 insertions(+), 23 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 91d02b1aa0..4d49fb7149 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2638,11 +2638,25 @@ impl Editor { let display_map = self.display_map.update(cx, |map, cx| map.snapshot(cx)); for selection in &mut selections { if selection.is_empty() { - let head = selection.head().to_display_point(&display_map); - let cursor = movement::left(&display_map, head) - .unwrap() - .to_point(&display_map); - selection.set_head(cursor); + let old_head = selection.head(); + let (buffer, line_buffer_range) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.row) + .unwrap(); + let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); + let mut new_head = + movement::left(&display_map, old_head.to_display_point(&display_map)) + .unwrap() + .to_point(&display_map); + if old_head.column <= indent_column && old_head.column > 0 { + let indent = buffer.indent_size(); + new_head = cmp::min( + new_head, + Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), + ); + } + + selection.set_head(new_head); selection.goal = SelectionGoal::None; } } @@ -7153,14 +7167,13 @@ mod tests { #[gpui::test] fn test_backspace(cx: &mut gpui::MutableAppContext) { - let buffer = - MultiBuffer::build_simple("one two three\nfour five six\nseven eight nine\nten\n", cx); let settings = Settings::test(&cx); let (_, view) = cx.add_window(Default::default(), |cx| { - build_editor(buffer.clone(), settings, cx) + build_editor(MultiBuffer::build_simple("", cx), settings, cx) }); view.update(cx, |view, cx| { + view.set_text("one two three\nfour five six\nseven eight nine\nten\n", cx); view.select_display_ranges( &[ // an empty selection - the preceding character is deleted @@ -7173,12 +7186,28 @@ mod tests { cx, ); view.backspace(&Backspace, cx); - }); + assert_eq!(view.text(cx), "oe two three\nfou five six\nseven ten\n"); - assert_eq!( - buffer.read(cx).read(cx).text(), - "oe two three\nfou five six\nseven ten\n" - ); + view.set_text(" one\n two\n three\n four", cx); + view.select_display_ranges( + &[ + // cursors at the the end of leading indent - last indent is deleted + DisplayPoint::new(0, 4)..DisplayPoint::new(0, 4), + DisplayPoint::new(1, 8)..DisplayPoint::new(1, 8), + // cursors inside leading indent - overlapping indent deletions are coalesced + DisplayPoint::new(2, 4)..DisplayPoint::new(2, 4), + DisplayPoint::new(2, 5)..DisplayPoint::new(2, 5), + DisplayPoint::new(2, 6)..DisplayPoint::new(2, 6), + // cursor at the beginning of a line - preceding newline is deleted + DisplayPoint::new(3, 0)..DisplayPoint::new(3, 0), + // selection inside leading indent - only the selected character is deleted + DisplayPoint::new(3, 2)..DisplayPoint::new(3, 3), + ], + cx, + ); + view.backspace(&Backspace, cx); + assert_eq!(view.text(cx), "one\n two\n three four"); + }); } #[gpui::test] diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 64683faa96..3678f8f116 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1657,7 +1657,7 @@ impl MultiBufferSnapshot { } } - fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range)> { + pub fn buffer_line_for_row(&self, row: u32) -> Option<(&BufferSnapshot, Range)> { let mut cursor = self.excerpts.cursor::(); cursor.seek(&Point::new(row, 0), Bias::Right, &()); if let Some(excerpt) = cursor.item() { diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index 3d79ecadd6..5f3ddb8b99 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -47,9 +47,6 @@ lazy_static! { static ref QUERY_CURSORS: Mutex> = Default::default(); } -// TODO - Make this configurable -const INDENT_SIZE: u32 = 4; - pub struct Buffer { text: TextBuffer, file: Option>, @@ -70,6 +67,7 @@ pub struct Buffer { file_update_count: usize, completion_triggers: Vec, deferred_ops: OperationQueue, + indent_size: u32, } pub struct BufferSnapshot { @@ -81,9 +79,9 @@ pub struct BufferSnapshot { file_update_count: usize, remote_selections: TreeMap, selections_update_count: usize, - is_parsing: bool, language: Option>, parse_count: usize, + indent_size: u32, } #[derive(Clone, Debug)] @@ -416,6 +414,8 @@ impl Buffer { file_update_count: 0, completion_triggers: Default::default(), deferred_ops: OperationQueue::new(), + // TODO: make this configurable + indent_size: 4, } } @@ -428,10 +428,10 @@ impl Buffer { diagnostics: self.diagnostics.clone(), diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, - is_parsing: self.parsing_in_background, language: self.language.clone(), parse_count: self.parse_count, selections_update_count: self.selections_update_count, + indent_size: self.indent_size, } } @@ -768,7 +768,11 @@ impl Buffer { .before_edit .indent_column_for_line(suggestion.basis_row) }); - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let delta = if suggestion.indent { + snapshot.indent_size + } else { + 0 + }; old_suggestions.insert( *old_to_new_rows.get(&old_row).unwrap(), indentation_basis + delta, @@ -787,7 +791,11 @@ impl Buffer { .into_iter() .flatten(); for (new_row, suggestion) in new_edited_row_range.zip(suggestions) { - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let delta = if suggestion.indent { + snapshot.indent_size + } else { + 0 + }; let new_indentation = indent_columns .get(&suggestion.basis_row) .copied() @@ -819,7 +827,11 @@ impl Buffer { .into_iter() .flatten(); for (row, suggestion) in inserted_row_range.zip(suggestions) { - let delta = if suggestion.indent { INDENT_SIZE } else { 0 }; + let delta = if suggestion.indent { + snapshot.indent_size + } else { + 0 + }; let new_indentation = indent_columns .get(&suggestion.basis_row) .copied() @@ -1868,6 +1880,10 @@ impl BufferSnapshot { pub fn file_update_count(&self) -> usize { self.file_update_count } + + pub fn indent_size(&self) -> u32 { + self.indent_size + } } impl Clone for BufferSnapshot { @@ -1881,9 +1897,9 @@ impl Clone for BufferSnapshot { selections_update_count: self.selections_update_count, diagnostics_update_count: self.diagnostics_update_count, file_update_count: self.file_update_count, - is_parsing: self.is_parsing, language: self.language.clone(), parse_count: self.parse_count, + indent_size: self.indent_size, } } } From 4bbf5ed0b979b633d52c736281b55ae7e8f7660f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2022 12:00:33 +0100 Subject: [PATCH 07/18] Listen to all LSP progress notifications and broadcast them to peers --- crates/project/src/project.rs | 286 +++++++++++++++++++++------------- crates/rpc/proto/zed.proto | 30 +++- crates/rpc/src/proto.rs | 6 +- crates/server/src/rpc.rs | 21 +-- 4 files changed, 207 insertions(+), 136 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9a45e08164..9741f16784 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -28,7 +28,6 @@ use rand::prelude::*; use search::SearchQuery; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; -use smol::block_on; use std::{ cell::RefCell, cmp::{self, Ordering}, @@ -115,6 +114,21 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), } +enum LspEvent { + WorkStart { + token: String, + }, + WorkProgress { + token: String, + message: Option, + percentage: Option, + }, + WorkEnd { + token: String, + }, + DiagnosticsUpdate(lsp::PublishDiagnosticsParams), +} + #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct ProjectPath { pub worktree_id: WorktreeId, @@ -203,8 +217,7 @@ impl Project { client.add_entity_message_handler(Self::handle_add_collaborator); client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updated); - client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updating); + client.add_entity_message_handler(Self::handle_lsp_event); client.add_entity_message_handler(Self::handle_remove_collaborator); client.add_entity_message_handler(Self::handle_register_worktree); client.add_entity_message_handler(Self::handle_unregister_worktree); @@ -1155,12 +1168,6 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - enum LspEvent { - DiagnosticsStart, - DiagnosticsUpdate(lsp::PublishDiagnosticsParams), - DiagnosticsFinish, - } - let key = (worktree_id, language.name()); self.started_language_servers .entry(key.clone()) @@ -1171,76 +1178,50 @@ impl Project { self.client.http_client(), cx, ); - let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { let mut language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; + let (lsp_events_tx, lsp_events_rx) = smol::channel::unbounded(); - let disk_based_sources = language - .disk_based_diagnostic_sources() - .cloned() - .unwrap_or_default(); - let disk_based_diagnostics_progress_token = - language.disk_based_diagnostics_progress_token().cloned(); - let has_disk_based_diagnostic_progress_token = - disk_based_diagnostics_progress_token.is_some(); - let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); - - // Listen for `PublishDiagnostics` notifications. language_server .on_notification::({ - let diagnostics_tx = diagnostics_tx.clone(); + let lsp_events_tx = lsp_events_tx.clone(); move |params| { - if !has_disk_based_diagnostic_progress_token { - block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok(); - } - block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))) + lsp_events_tx + .try_send(LspEvent::DiagnosticsUpdate(params)) .ok(); - if !has_disk_based_diagnostic_progress_token { - block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok(); - } } }) .detach(); - // Listen for `Progress` notifications. Send an event when the language server - // transitions between running jobs and not running any jobs. - let mut running_jobs_for_this_server: i32 = 0; language_server .on_notification::(move |params| { let token = match params.token { - lsp::NumberOrString::Number(_) => None, - lsp::NumberOrString::String(token) => Some(token), + lsp::NumberOrString::String(token) => token, + lsp::NumberOrString::Number(token) => { + log::info!("skipping numeric progress token {}", token); + return; + } }; - if token == disk_based_diagnostics_progress_token { - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => { - match progress { - lsp::WorkDoneProgress::Begin(_) => { - running_jobs_for_this_server += 1; - if running_jobs_for_this_server == 1 { - block_on( - diagnostics_tx - .send(LspEvent::DiagnosticsStart), - ) - .ok(); - } - } - lsp::WorkDoneProgress::End(_) => { - running_jobs_for_this_server -= 1; - if running_jobs_for_this_server == 0 { - block_on( - diagnostics_tx - .send(LspEvent::DiagnosticsFinish), - ) - .ok(); - } - } - _ => {} - } + match params.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + lsp_events_tx.try_send(LspEvent::WorkStart { token }).ok(); } - } + lsp::WorkDoneProgress::Report(report) => { + lsp_events_tx + .try_send(LspEvent::WorkProgress { + token, + message: report.message, + percentage: report.percentage.map(|p| p as usize), + }) + .ok(); + } + lsp::WorkDoneProgress::End(_) => { + lsp_events_tx.try_send(LspEvent::WorkEnd { token }).ok(); + } + }, } }) .detach(); @@ -1249,43 +1230,11 @@ impl Project { cx.spawn(|mut cx| { let this = this.downgrade(); async move { - while let Ok(message) = diagnostics_rx.recv().await { + while let Ok(event) = lsp_events_rx.recv().await { let this = this.upgrade(&cx)?; - match message { - LspEvent::DiagnosticsStart => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_started(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdating { - project_id, - }) - .log_err(); - } - }); - } - LspEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); - this.update(&mut cx, |this, cx| { - this.update_diagnostics( - params, - &disk_based_sources, - cx, - ) - .log_err(); - }); - } - LspEvent::DiagnosticsFinish => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_finished(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdated { - project_id, - }) - .log_err(); - } - }); - } - } + this.update(&mut cx, |this, cx| { + this.on_local_lsp_event(event, &language, cx) + }); } Some(()) } @@ -1358,6 +1307,107 @@ impl Project { }); } + fn on_local_lsp_event( + &mut self, + event: LspEvent, + language: &Arc, + cx: &mut ModelContext, + ) { + let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); + match event { + LspEvent::WorkStart { token } => { + if Some(&token) == disk_diagnostics_token { + self.disk_based_diagnostics_started(cx); + self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + )); + } else { + self.on_lsp_work_start(token.clone(), cx); + self.send_lsp_event(proto::lsp_event::Variant::WorkStart( + proto::LspWorkStart { token }, + )); + } + } + LspEvent::WorkProgress { + token, + message, + percentage, + } => { + if Some(&token) != disk_diagnostics_token { + self.on_lsp_work_progress(token.clone(), message.clone(), percentage, cx); + self.send_lsp_event(proto::lsp_event::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message, + percentage: percentage.map(|p| p as u32), + }, + )); + } + } + LspEvent::WorkEnd { token } => { + if Some(&token) == disk_diagnostics_token { + self.disk_based_diagnostics_finished(cx); + self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + )); + } else { + self.on_lsp_work_end(token.clone(), cx); + self.send_lsp_event(proto::lsp_event::Variant::WorkEnd(proto::LspWorkEnd { + token, + })); + } + } + LspEvent::DiagnosticsUpdate(mut params) => { + language.process_diagnostics(&mut params); + + if disk_diagnostics_token.is_none() { + self.disk_based_diagnostics_started(cx); + self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + )); + } + self.update_diagnostics( + params, + language + .disk_based_diagnostic_sources() + .unwrap_or(&Default::default()), + cx, + ) + .log_err(); + if disk_diagnostics_token.is_none() { + self.disk_based_diagnostics_finished(cx); + self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + )); + } + } + } + } + + fn on_lsp_work_start(&mut self, token: String, cx: &mut ModelContext) {} + + fn on_lsp_work_progress( + &mut self, + token: String, + message: Option, + percentage: Option, + cx: &mut ModelContext, + ) { + } + + fn on_lsp_work_end(&mut self, token: String, cx: &mut ModelContext) {} + + fn send_lsp_event(&self, event: proto::lsp_event::Variant) { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::LspEvent { + project_id, + variant: Some(event), + }) + .log_err(); + } + } + pub fn update_diagnostics( &mut self, params: lsp::PublishDiagnosticsParams, @@ -3096,23 +3146,41 @@ impl Project { }) } - async fn handle_disk_based_diagnostics_updating( + async fn handle_lsp_event( this: ModelHandle, - _: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.disk_based_diagnostics_started(cx)); - Ok(()) - } + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid variant"))? + { + proto::lsp_event::Variant::WorkStart(payload) => this.update(&mut cx, |this, cx| { + this.on_lsp_work_start(payload.token, cx); + }), + proto::lsp_event::Variant::WorkProgress(payload) => this.update(&mut cx, |this, cx| { + this.on_lsp_work_progress( + payload.token, + payload.message, + payload.percentage.map(|p| p as usize), + cx, + ); + }), + proto::lsp_event::Variant::WorkEnd(payload) => this.update(&mut cx, |this, cx| { + this.on_lsp_work_end(payload.token, cx); + }), + proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating(_) => { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_started(cx); + }) + } + proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated(_) => { + this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); + } + } - async fn handle_disk_based_diagnostics_updated( - this: ModelHandle, - _: TypedEnvelope, - _: Arc, - mut cx: AsyncAppContext, - ) -> Result<()> { - this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7f43aaff1a..18df77e5c3 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -37,8 +37,7 @@ message Envelope { UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; UpdateDiagnosticSummary update_diagnostic_summary = 32; - DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 33; - DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 34; + LspEvent lsp_event = 33; OpenBuffer open_buffer = 35; OpenBufferResponse open_buffer_response = 36; @@ -424,14 +423,35 @@ message DiagnosticSummary { uint32 hint_count = 5; } -message DiskBasedDiagnosticsUpdating { +message LspEvent { uint64 project_id = 1; + oneof variant { + LspWorkStart work_start = 2; + LspWorkProgress work_progress = 3; + LspWorkEnd work_end = 4; + LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 5; + LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 6; + } } -message DiskBasedDiagnosticsUpdated { - uint64 project_id = 1; +message LspWorkStart { + string token = 1; } +message LspWorkProgress { + string token = 1; + optional string message = 2; + optional uint32 percentage = 3; +} + +message LspWorkEnd { + string token = 1; +} + +message LspDiskBasedDiagnosticsUpdating {} + +message LspDiskBasedDiagnosticsUpdated {} + message GetChannels {} message GetChannelsResponse { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d252decb3a..15a5839524 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -146,8 +146,6 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), - (DiskBasedDiagnosticsUpdated, Background), - (DiskBasedDiagnosticsUpdating, Background), (Error, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), @@ -175,6 +173,7 @@ messages!( (JoinProjectResponse, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), + (LspEvent, Background), (OpenBuffer, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), @@ -246,8 +245,6 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, - DiskBasedDiagnosticsUpdated, - DiskBasedDiagnosticsUpdating, FormatBuffers, GetCodeActions, GetCompletions, @@ -257,6 +254,7 @@ entity_messages!( GetProjectSymbols, JoinProject, LeaveProject, + LspEvent, OpenBuffer, OpenBufferForSymbol, PerformRename, diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 2b954b5774..74406146c1 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -84,8 +84,7 @@ impl Server { .add_message_handler(Server::unregister_worktree) .add_request_handler(Server::update_worktree) .add_message_handler(Server::update_diagnostic_summary) - .add_message_handler(Server::disk_based_diagnostics_updating) - .add_message_handler(Server::disk_based_diagnostics_updated) + .add_message_handler(Server::lsp_event) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) @@ -535,23 +534,9 @@ impl Server { Ok(()) } - async fn disk_based_diagnostics_updating( + async fn lsp_event( self: Arc, - request: TypedEnvelope, - ) -> tide::Result<()> { - let receiver_ids = self - .state() - .project_connection_ids(request.payload.project_id, request.sender_id)?; - broadcast(request.sender_id, receiver_ids, |connection_id| { - self.peer - .forward_send(request.sender_id, connection_id, request.payload.clone()) - })?; - Ok(()) - } - - async fn disk_based_diagnostics_updated( - self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { let receiver_ids = self .state() From 4243f0c339166cad2ba7ccc7c8731134b9d85b7f Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2022 16:09:47 +0100 Subject: [PATCH 08/18] Render pending language server work in status bar --- crates/project/src/project.rs | 162 +++++++++++++++++++++-------- crates/rpc/proto/zed.proto | 11 +- crates/workspace/src/lsp_status.rs | 31 +++++- crates/zed/src/zed.rs | 1 + 4 files changed, 151 insertions(+), 54 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9741f16784..95609bf43f 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -51,6 +51,8 @@ pub struct Project { languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, + pending_language_server_work: HashMap<(usize, String), LspWorkProgress>, + next_language_server_id: usize, client: Arc, user_store: ModelHandle, fs: Arc, @@ -120,8 +122,7 @@ enum LspEvent { }, WorkProgress { token: String, - message: Option, - percentage: Option, + progress: LspWorkProgress, }, WorkEnd { token: String, @@ -129,6 +130,12 @@ enum LspEvent { DiagnosticsUpdate(lsp::PublishDiagnosticsParams), } +#[derive(Clone, Default)] +pub struct LspWorkProgress { + pub message: Option, + pub percentage: Option, +} + #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct ProjectPath { pub worktree_id: WorktreeId, @@ -317,6 +324,8 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), + pending_language_server_work: Default::default(), + next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), } }) @@ -386,6 +395,8 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), + pending_language_server_work: Default::default(), + next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), @@ -1172,6 +1183,7 @@ impl Project { self.started_language_servers .entry(key.clone()) .or_insert_with(|| { + let server_id = post_inc(&mut self.next_language_server_id); let language_server = self.languages.start_language_server( language.clone(), worktree_path, @@ -1213,8 +1225,12 @@ impl Project { lsp_events_tx .try_send(LspEvent::WorkProgress { token, - message: report.message, - percentage: report.percentage.map(|p| p as usize), + progress: LspWorkProgress { + message: report.message, + percentage: report + .percentage + .map(|p| p as usize), + }, }) .ok(); } @@ -1233,7 +1249,7 @@ impl Project { while let Ok(event) = lsp_events_rx.recv().await { let this = this.upgrade(&cx)?; this.update(&mut cx, |this, cx| { - this.on_local_lsp_event(event, &language, cx) + this.on_local_lsp_event(server_id, event, &language, cx) }); } Some(()) @@ -1309,6 +1325,7 @@ impl Project { fn on_local_lsp_event( &mut self, + language_server_id: usize, event: LspEvent, language: &Arc, cx: &mut ModelContext, @@ -1318,43 +1335,53 @@ impl Project { LspEvent::WorkStart { token } => { if Some(&token) == disk_diagnostics_token { self.disk_based_diagnostics_started(cx); - self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - )); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); } else { - self.on_lsp_work_start(token.clone(), cx); - self.send_lsp_event(proto::lsp_event::Variant::WorkStart( - proto::LspWorkStart { token }, - )); + self.on_lsp_work_start(language_server_id, token.clone(), cx); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::WorkStart(proto::LspWorkStart { token }), + ); } } - LspEvent::WorkProgress { - token, - message, - percentage, - } => { + LspEvent::WorkProgress { token, progress } => { if Some(&token) != disk_diagnostics_token { - self.on_lsp_work_progress(token.clone(), message.clone(), percentage, cx); - self.send_lsp_event(proto::lsp_event::Variant::WorkProgress( - proto::LspWorkProgress { + self.on_lsp_work_progress( + language_server_id, + token.clone(), + progress.clone(), + cx, + ); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::WorkProgress(proto::LspWorkProgress { token, - message, - percentage: percentage.map(|p| p as u32), - }, - )); + message: progress.message, + percentage: progress.percentage.map(|p| p as u32), + }), + ); } } LspEvent::WorkEnd { token } => { if Some(&token) == disk_diagnostics_token { self.disk_based_diagnostics_finished(cx); - self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - )); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); } else { - self.on_lsp_work_end(token.clone(), cx); - self.send_lsp_event(proto::lsp_event::Variant::WorkEnd(proto::LspWorkEnd { - token, - })); + self.on_lsp_work_end(language_server_id, token.clone(), cx); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::WorkEnd(proto::LspWorkEnd { token }), + ); } } LspEvent::DiagnosticsUpdate(mut params) => { @@ -1362,9 +1389,12 @@ impl Project { if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_started(cx); - self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - )); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); } self.update_diagnostics( params, @@ -1376,38 +1406,74 @@ impl Project { .log_err(); if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_finished(cx); - self.send_lsp_event(proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - )); + self.broadcast_lsp_event( + language_server_id, + proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); } } } } - fn on_lsp_work_start(&mut self, token: String, cx: &mut ModelContext) {} + fn on_lsp_work_start( + &mut self, + language_server_id: usize, + token: String, + cx: &mut ModelContext, + ) { + self.pending_language_server_work.insert( + (language_server_id, token), + LspWorkProgress { + message: None, + percentage: None, + }, + ); + cx.notify(); + } fn on_lsp_work_progress( &mut self, + language_server_id: usize, token: String, - message: Option, - percentage: Option, + progress: LspWorkProgress, cx: &mut ModelContext, ) { + self.pending_language_server_work + .insert((language_server_id, token), progress); + cx.notify(); } - fn on_lsp_work_end(&mut self, token: String, cx: &mut ModelContext) {} + fn on_lsp_work_end( + &mut self, + language_server_id: usize, + token: String, + cx: &mut ModelContext, + ) { + self.pending_language_server_work + .remove(&(language_server_id, token)); + cx.notify(); + } - fn send_lsp_event(&self, event: proto::lsp_event::Variant) { + fn broadcast_lsp_event(&self, language_server_id: usize, event: proto::lsp_event::Variant) { if let Some(project_id) = self.remote_id() { self.client .send(proto::LspEvent { project_id, + language_server_id: language_server_id as u64, variant: Some(event), }) .log_err(); } } + pub fn pending_language_server_work(&self) -> impl Iterator { + self.pending_language_server_work + .iter() + .map(|((_, token), progress)| (token.as_str(), progress)) + } + pub fn update_diagnostics( &mut self, params: lsp::PublishDiagnosticsParams, @@ -3152,24 +3218,28 @@ impl Project { _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { + let language_server_id = envelope.payload.language_server_id as usize; match envelope .payload .variant .ok_or_else(|| anyhow!("invalid variant"))? { proto::lsp_event::Variant::WorkStart(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_start(payload.token, cx); + this.on_lsp_work_start(language_server_id, payload.token, cx); }), proto::lsp_event::Variant::WorkProgress(payload) => this.update(&mut cx, |this, cx| { this.on_lsp_work_progress( + language_server_id, payload.token, - payload.message, - payload.percentage.map(|p| p as usize), + LspWorkProgress { + message: payload.message, + percentage: payload.percentage.map(|p| p as usize), + }, cx, ); }), proto::lsp_event::Variant::WorkEnd(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_end(payload.token, cx); + this.on_lsp_work_end(language_server_id, payload.token, cx); }), proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating(_) => { this.update(&mut cx, |this, cx| { diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 18df77e5c3..c9895739d9 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -425,12 +425,13 @@ message DiagnosticSummary { message LspEvent { uint64 project_id = 1; + uint64 language_server_id = 2; oneof variant { - LspWorkStart work_start = 2; - LspWorkProgress work_progress = 3; - LspWorkEnd work_end = 4; - LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 5; - LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 6; + LspWorkStart work_start = 3; + LspWorkProgress work_progress = 4; + LspWorkEnd work_end = 5; + LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6; + LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; } } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index ee61ecf24d..98bd0112a9 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,11 +1,13 @@ use crate::{ItemViewHandle, Settings, StatusItemView}; use futures::StreamExt; use gpui::{ - action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View, - ViewContext, + action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, + RenderContext, View, ViewContext, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use postage::watch; +use project::Project; +use std::fmt::Write; use std::sync::Arc; action!(DismissErrorMessage); @@ -15,6 +17,7 @@ pub struct LspStatus { checking_for_update: Vec, downloading: Vec, failed: Vec, + project: ModelHandle, } pub fn init(cx: &mut MutableAppContext) { @@ -23,6 +26,7 @@ pub fn init(cx: &mut MutableAppContext) { impl LspStatus { pub fn new( + project: &ModelHandle, languages: Arc, settings_rx: watch::Receiver, cx: &mut ViewContext, @@ -62,11 +66,14 @@ impl LspStatus { } }) .detach(); + cx.observe(project, |_, _, cx| cx.notify()).detach(); + Self { settings_rx, checking_for_update: Default::default(), downloading: Default::default(), failed: Default::default(), + project: project.clone(), } } @@ -87,7 +94,24 @@ impl View for LspStatus { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings_rx.borrow().theme; - if !self.downloading.is_empty() { + + let mut pending_work = self.project.read(cx).pending_language_server_work(); + if let Some((progress_token, progress)) = pending_work.next() { + let mut message = progress + .message + .clone() + .unwrap_or_else(|| progress_token.to_string()); + if let Some(percentage) = progress.percentage { + write!(&mut message, " ({}%)", percentage).unwrap(); + } + + let additional_work_count = pending_work.count(); + if additional_work_count > 0 { + write!(&mut message, " + {} more", additional_work_count).unwrap(); + } + + Label::new(message, theme.workspace.status_bar.lsp_message.clone()).boxed() + } else if !self.downloading.is_empty() { Label::new( format!( "Downloading {} language server{}...", @@ -112,6 +136,7 @@ impl View for LspStatus { ) .boxed() } else if !self.failed.is_empty() { + drop(pending_work); MouseEventHandler::new::(0, cx, |_, _| { Label::new( format!( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index c9a14bf236..c513155e6f 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -101,6 +101,7 @@ pub fn build_workspace( }); let lsp_status = cx.add_view(|cx| { workspace::lsp_status::LspStatus::new( + workspace.project(), app_state.languages.clone(), app_state.settings.clone(), cx, From 45fb470f4d8b16801f530a0cf4ef05d6fb0242b7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2022 16:45:13 +0100 Subject: [PATCH 09/18] Display language server name in status bar --- crates/lsp/src/lsp.rs | 22 ++- crates/project/src/project.rs | 207 +++++++++++++++++++---------- crates/rpc/proto/zed.proto | 16 ++- crates/rpc/src/proto.rs | 6 +- crates/server/src/rpc.rs | 28 +++- crates/server/src/rpc/store.rs | 20 +++ crates/workspace/src/lsp_status.rs | 15 ++- 7 files changed, 228 insertions(+), 86 deletions(-) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d9024975e4..43b26efc37 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -35,6 +35,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: channel::Sender>, + name: String, capabilities: ServerCapabilities, notification_handlers: Arc>>, response_handlers: Arc>>, @@ -118,9 +119,11 @@ impl LanguageServer { .spawn()?; let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); - Ok(Self::new_internal( - stdin, stdout, root_path, options, background, - )) + let mut server = Self::new_internal(stdin, stdout, root_path, options, background); + if let Some(name) = binary_path.file_name() { + server.name = name.to_string_lossy().to_string(); + } + Ok(server) } fn new_internal( @@ -222,6 +225,7 @@ impl LanguageServer { Self { notification_handlers, response_handlers, + name: Default::default(), capabilities: Default::default(), next_id: Default::default(), outbound_tx, @@ -292,7 +296,13 @@ impl LanguageServer { }; let response = this.request::(params).await?; - Arc::get_mut(&mut this).unwrap().capabilities = response.capabilities; + { + let this = Arc::get_mut(&mut this).unwrap(); + if let Some(info) = response.server_info { + this.name = info.name; + } + this.capabilities = response.capabilities; + } this.notify::(InitializedParams {})?; Ok(this) } @@ -355,6 +365,10 @@ impl LanguageServer { } } + pub fn name<'a>(self: &'a Arc) -> &'a str { + &self.name + } + pub fn capabilities<'a>(self: &'a Arc) -> &'a ServerCapabilities { &self.capabilities } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 95609bf43f..a35b8dc79a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -7,7 +7,7 @@ pub mod worktree; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; -use collections::{hash_map, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ @@ -51,7 +51,8 @@ pub struct Project { languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, - pending_language_server_work: HashMap<(usize, String), LspWorkProgress>, + pending_language_server_work: BTreeMap<(usize, String), LanguageServerProgress>, + language_server_names: HashMap, next_language_server_id: usize, client: Arc, user_store: ModelHandle, @@ -116,13 +117,13 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), } -enum LspEvent { +enum LanguageServerEvent { WorkStart { token: String, }, WorkProgress { token: String, - progress: LspWorkProgress, + progress: LanguageServerProgress, }, WorkEnd { token: String, @@ -131,7 +132,7 @@ enum LspEvent { } #[derive(Clone, Default)] -pub struct LspWorkProgress { +pub struct LanguageServerProgress { pub message: Option, pub percentage: Option, } @@ -224,7 +225,8 @@ impl Project { client.add_entity_message_handler(Self::handle_add_collaborator); client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_lsp_event); + client.add_entity_message_handler(Self::handle_start_language_server); + client.add_entity_message_handler(Self::handle_update_language_server); client.add_entity_message_handler(Self::handle_remove_collaborator); client.add_entity_message_handler(Self::handle_register_worktree); client.add_entity_message_handler(Self::handle_unregister_worktree); @@ -325,6 +327,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), pending_language_server_work: Default::default(), + language_server_names: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), } @@ -396,6 +399,11 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), pending_language_server_work: Default::default(), + language_server_names: response + .language_servers + .into_iter() + .map(|s| (s.id as usize, s.name)) + .collect(), next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), @@ -1193,14 +1201,15 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let mut language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; - let (lsp_events_tx, lsp_events_rx) = smol::channel::unbounded(); + let (language_server_events_tx, language_server_events_rx) = + smol::channel::unbounded(); language_server .on_notification::({ - let lsp_events_tx = lsp_events_tx.clone(); + let language_server_events_tx = language_server_events_tx.clone(); move |params| { - lsp_events_tx - .try_send(LspEvent::DiagnosticsUpdate(params)) + language_server_events_tx + .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) .ok(); } }) @@ -1219,13 +1228,15 @@ impl Project { match params.value { lsp::ProgressParamsValue::WorkDone(progress) => match progress { lsp::WorkDoneProgress::Begin(_) => { - lsp_events_tx.try_send(LspEvent::WorkStart { token }).ok(); + language_server_events_tx + .try_send(LanguageServerEvent::WorkStart { token }) + .ok(); } lsp::WorkDoneProgress::Report(report) => { - lsp_events_tx - .try_send(LspEvent::WorkProgress { + language_server_events_tx + .try_send(LanguageServerEvent::WorkProgress { token, - progress: LspWorkProgress { + progress: LanguageServerProgress { message: report.message, percentage: report .percentage @@ -1235,7 +1246,9 @@ impl Project { .ok(); } lsp::WorkDoneProgress::End(_) => { - lsp_events_tx.try_send(LspEvent::WorkEnd { token }).ok(); + language_server_events_tx + .try_send(LanguageServerEvent::WorkEnd { token }) + .ok(); } }, } @@ -1246,10 +1259,10 @@ impl Project { cx.spawn(|mut cx| { let this = this.downgrade(); async move { - while let Ok(event) = lsp_events_rx.recv().await { + while let Ok(event) = language_server_events_rx.recv().await { let this = this.upgrade(&cx)?; this.update(&mut cx, |this, cx| { - this.on_local_lsp_event(server_id, event, &language, cx) + this.on_lsp_event(server_id, event, &language, cx) }); } Some(()) @@ -1261,6 +1274,20 @@ impl Project { this.update(&mut cx, |this, cx| { this.language_servers .insert(key.clone(), language_server.clone()); + this.language_server_names + .insert(server_id, language_server.name().to_string()); + + if let Some(project_id) = this.remote_id() { + this.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id as u64, + name: language_server.name().to_string(), + }), + }) + .log_err(); + } // Tell the language server about every open buffer in the worktree that matches the language. for buffer in this.opened_buffers.values() { @@ -1315,6 +1342,7 @@ impl Project { } } + cx.notify(); Some(()) }); @@ -1323,33 +1351,35 @@ impl Project { }); } - fn on_local_lsp_event( + fn on_lsp_event( &mut self, language_server_id: usize, - event: LspEvent, + event: LanguageServerEvent, language: &Arc, cx: &mut ModelContext, ) { let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); match event { - LspEvent::WorkStart { token } => { + LanguageServerEvent::WorkStart { token } => { if Some(&token) == disk_diagnostics_token { self.disk_based_diagnostics_started(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( proto::LspDiskBasedDiagnosticsUpdating {}, ), ); } else { self.on_lsp_work_start(language_server_id, token.clone(), cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::WorkStart(proto::LspWorkStart { token }), + proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { + token, + }), ); } } - LspEvent::WorkProgress { token, progress } => { + LanguageServerEvent::WorkProgress { token, progress } => { if Some(&token) != disk_diagnostics_token { self.on_lsp_work_progress( language_server_id, @@ -1357,41 +1387,45 @@ impl Project { progress.clone(), cx, ); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::WorkProgress(proto::LspWorkProgress { - token, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - }), + proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: progress.message, + percentage: progress.percentage.map(|p| p as u32), + }, + ), ); } } - LspEvent::WorkEnd { token } => { + LanguageServerEvent::WorkEnd { token } => { if Some(&token) == disk_diagnostics_token { self.disk_based_diagnostics_finished(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, ), ); } else { self.on_lsp_work_end(language_server_id, token.clone(), cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::WorkEnd(proto::LspWorkEnd { token }), + proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + token, + }), ); } } - LspEvent::DiagnosticsUpdate(mut params) => { + LanguageServerEvent::DiagnosticsUpdate(mut params) => { language.process_diagnostics(&mut params); if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_started(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( proto::LspDiskBasedDiagnosticsUpdating {}, ), ); @@ -1406,9 +1440,9 @@ impl Project { .log_err(); if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_finished(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, ), ); @@ -1425,7 +1459,7 @@ impl Project { ) { self.pending_language_server_work.insert( (language_server_id, token), - LspWorkProgress { + LanguageServerProgress { message: None, percentage: None, }, @@ -1437,7 +1471,7 @@ impl Project { &mut self, language_server_id: usize, token: String, - progress: LspWorkProgress, + progress: LanguageServerProgress, cx: &mut ModelContext, ) { self.pending_language_server_work @@ -1456,10 +1490,14 @@ impl Project { cx.notify(); } - fn broadcast_lsp_event(&self, language_server_id: usize, event: proto::lsp_event::Variant) { + fn broadcast_language_server_update( + &self, + language_server_id: usize, + event: proto::update_language_server::Variant, + ) { if let Some(project_id) = self.remote_id() { self.client - .send(proto::LspEvent { + .send(proto::UpdateLanguageServer { project_id, language_server_id: language_server_id as u64, variant: Some(event), @@ -1468,10 +1506,15 @@ impl Project { } } - pub fn pending_language_server_work(&self) -> impl Iterator { - self.pending_language_server_work - .iter() - .map(|((_, token), progress)| (token.as_str(), progress)) + pub fn pending_language_server_work( + &self, + ) -> impl Iterator { + self.pending_language_server_work.iter().filter_map( + |((language_server_id, token), progress)| { + let name = self.language_server_names.get(language_server_id)?; + Some((name.as_str(), token.as_str(), progress)) + }, + ) } pub fn update_diagnostics( @@ -3212,9 +3255,27 @@ impl Project { }) } - async fn handle_lsp_event( + async fn handle_start_language_server( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let server = envelope + .payload + .server + .ok_or_else(|| anyhow!("invalid server"))?; + this.update(&mut cx, |this, cx| { + this.language_server_names + .insert(server.id as usize, server.name); + cx.notify(); + }); + Ok(()) + } + + async fn handle_update_language_server( + this: ModelHandle, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { @@ -3224,29 +3285,35 @@ impl Project { .variant .ok_or_else(|| anyhow!("invalid variant"))? { - proto::lsp_event::Variant::WorkStart(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_start(language_server_id, payload.token, cx); - }), - proto::lsp_event::Variant::WorkProgress(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_progress( - language_server_id, - payload.token, - LspWorkProgress { - message: payload.message, - percentage: payload.percentage.map(|p| p as usize), - }, - cx, - ); - }), - proto::lsp_event::Variant::WorkEnd(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_end(language_server_id, payload.token, cx); - }), - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating(_) => { + proto::update_language_server::Variant::WorkStart(payload) => { + this.update(&mut cx, |this, cx| { + this.on_lsp_work_start(language_server_id, payload.token, cx); + }) + } + proto::update_language_server::Variant::WorkProgress(payload) => { + this.update(&mut cx, |this, cx| { + this.on_lsp_work_progress( + language_server_id, + payload.token, + LanguageServerProgress { + message: payload.message, + percentage: payload.percentage.map(|p| p as usize), + }, + cx, + ); + }) + } + proto::update_language_server::Variant::WorkEnd(payload) => { + this.update(&mut cx, |this, cx| { + this.on_lsp_work_end(language_server_id, payload.token, cx); + }) + } + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => { this.update(&mut cx, |this, cx| { this.disk_based_diagnostics_started(cx); }) } - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated(_) => { + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c9895739d9..87303c3c26 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -37,7 +37,8 @@ message Envelope { UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; UpdateDiagnosticSummary update_diagnostic_summary = 32; - LspEvent lsp_event = 33; + StartLanguageServer start_language_server = 33; + UpdateLanguageServer update_language_server = 34; OpenBuffer open_buffer = 35; OpenBufferResponse open_buffer_response = 36; @@ -121,6 +122,7 @@ message JoinProjectResponse { uint32 replica_id = 1; repeated Worktree worktrees = 2; repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; } message LeaveProject { @@ -409,6 +411,16 @@ message LocalTimestamp { uint32 value = 2; } +message LanguageServer { + uint64 id = 1; + string name = 2; +} + +message StartLanguageServer { + uint64 project_id = 1; + LanguageServer server = 2; +} + message UpdateDiagnosticSummary { uint64 project_id = 1; uint64 worktree_id = 2; @@ -423,7 +435,7 @@ message DiagnosticSummary { uint32 hint_count = 5; } -message LspEvent { +message UpdateLanguageServer { uint64 project_id = 1; uint64 language_server_id = 2; oneof variant { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 15a5839524..54b26b830c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -171,9 +171,10 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), + (StartLanguageServer, Foreground), + (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), - (LspEvent, Background), (OpenBuffer, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), @@ -254,7 +255,6 @@ entity_messages!( GetProjectSymbols, JoinProject, LeaveProject, - LspEvent, OpenBuffer, OpenBufferForSymbol, PerformRename, @@ -262,11 +262,13 @@ entity_messages!( RemoveProjectCollaborator, SaveBuffer, SearchProject, + StartLanguageServer, UnregisterWorktree, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateLanguageServer, RegisterWorktree, UpdateWorktree, ); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 74406146c1..393e54165f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -83,8 +83,9 @@ impl Server { .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) .add_request_handler(Server::update_worktree) + .add_message_handler(Server::start_language_server) + .add_message_handler(Server::update_language_server) .add_message_handler(Server::update_diagnostic_summary) - .add_message_handler(Server::lsp_event) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) @@ -385,6 +386,7 @@ impl Server { worktrees, replica_id: joined.replica_id as u32, collaborators, + language_servers: joined.project.language_servers.clone(), }; let connection_ids = joined.project.connection_ids(); let contact_user_ids = joined.project.authorized_user_ids(); @@ -534,9 +536,29 @@ impl Server { Ok(()) } - async fn lsp_event( + async fn start_language_server( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + let receiver_ids = self.state_mut().start_language_server( + request.payload.project_id, + request.sender_id, + request + .payload + .server + .clone() + .ok_or_else(|| anyhow!("invalid language server"))?, + )?; + broadcast(request.sender_id, receiver_ids, |connection_id| { + self.peer + .forward_send(request.sender_id, connection_id, request.payload.clone()) + })?; + Ok(()) + } + + async fn update_language_server( self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { let receiver_ids = self .state() diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index c18db3b684..6f5252fecf 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -25,6 +25,7 @@ pub struct Project { pub host_user_id: UserId, pub share: Option, pub worktrees: HashMap, + pub language_servers: Vec, } pub struct Worktree { @@ -240,6 +241,7 @@ impl Store { host_user_id, share: None, worktrees: Default::default(), + language_servers: Default::default(), }, ); self.next_project_id += 1; @@ -438,6 +440,24 @@ impl Store { Err(anyhow!("no such worktree"))? } + pub fn start_language_server( + &mut self, + project_id: u64, + connection_id: ConnectionId, + language_server: proto::LanguageServer, + ) -> tide::Result> { + let project = self + .projects + .get_mut(&project_id) + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection_id == connection_id { + project.language_servers.push(language_server); + return Ok(project.connection_ids()); + } + + Err(anyhow!("no such project"))? + } + pub fn join_project( &mut self, connection_id: ConnectionId, diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index 98bd0112a9..e2976824b5 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -96,11 +96,16 @@ impl View for LspStatus { let theme = &self.settings_rx.borrow().theme; let mut pending_work = self.project.read(cx).pending_language_server_work(); - if let Some((progress_token, progress)) = pending_work.next() { - let mut message = progress - .message - .clone() - .unwrap_or_else(|| progress_token.to_string()); + if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { + let mut message = lang_server_name.to_string(); + + message.push_str(": "); + if let Some(progress_message) = progress.message.as_ref() { + message.push_str(progress_message); + } else { + message.push_str(progress_token); + } + if let Some(percentage) = progress.percentage { write!(&mut message, " ({}%)", percentage).unwrap(); } From 5157b428961929446930efbf9c70df24b014e7e7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Thu, 10 Mar 2022 17:04:36 +0100 Subject: [PATCH 10/18] Extract a `LanguageServerStatus` struct --- crates/project/src/project.rs | 130 ++++++++++++++++++----------- crates/workspace/src/lsp_status.rs | 26 +++++- 2 files changed, 106 insertions(+), 50 deletions(-) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index a35b8dc79a..dd4a17c13a 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -51,8 +51,7 @@ pub struct Project { languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, - pending_language_server_work: BTreeMap<(usize, String), LanguageServerProgress>, - language_server_names: HashMap, + language_server_statuses: BTreeMap, next_language_server_id: usize, client: Arc, user_store: ModelHandle, @@ -131,6 +130,12 @@ enum LanguageServerEvent { DiagnosticsUpdate(lsp::PublishDiagnosticsParams), } +pub struct LanguageServerStatus { + pub name: String, + pub pending_work: BTreeMap, + pending_diagnostic_updates: isize, +} + #[derive(Clone, Default)] pub struct LanguageServerProgress { pub message: Option, @@ -326,8 +331,7 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), - pending_language_server_work: Default::default(), - language_server_names: Default::default(), + language_server_statuses: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), } @@ -398,11 +402,19 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), - pending_language_server_work: Default::default(), - language_server_names: response + language_server_statuses: response .language_servers .into_iter() - .map(|s| (s.id as usize, s.name)) + .map(|server| { + ( + server.id as usize, + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + pending_diagnostic_updates: 0, + }, + ) + }) .collect(), next_language_server_id: 0, opened_buffers: Default::default(), @@ -1274,8 +1286,14 @@ impl Project { this.update(&mut cx, |this, cx| { this.language_servers .insert(key.clone(), language_server.clone()); - this.language_server_names - .insert(server_id, language_server.name().to_string()); + this.language_server_statuses.insert( + server_id, + LanguageServerStatus { + name: language_server.name().to_string(), + pending_work: Default::default(), + pending_diagnostic_updates: 0, + }, + ); if let Some(project_id) = this.remote_id() { this.client @@ -1359,16 +1377,26 @@ impl Project { cx: &mut ModelContext, ) { let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); + let language_server_status = + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status + } else { + return; + }; + match event { LanguageServerEvent::WorkStart { token } => { if Some(&token) == disk_diagnostics_token { - self.disk_based_diagnostics_started(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( - proto::LspDiskBasedDiagnosticsUpdating {}, - ), - ); + language_server_status.pending_diagnostic_updates += 1; + if language_server_status.pending_diagnostic_updates == 1 { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } } else { self.on_lsp_work_start(language_server_id, token.clone(), cx); self.broadcast_language_server_update( @@ -1401,13 +1429,16 @@ impl Project { } LanguageServerEvent::WorkEnd { token } => { if Some(&token) == disk_diagnostics_token { - self.disk_based_diagnostics_finished(cx); - self.broadcast_language_server_update( - language_server_id, - proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( - proto::LspDiskBasedDiagnosticsUpdated {}, - ), - ); + language_server_status.pending_diagnostic_updates -= 1; + if language_server_status.pending_diagnostic_updates == 0 { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } } else { self.on_lsp_work_end(language_server_id, token.clone(), cx); self.broadcast_language_server_update( @@ -1457,14 +1488,16 @@ impl Project { token: String, cx: &mut ModelContext, ) { - self.pending_language_server_work.insert( - (language_server_id, token), - LanguageServerProgress { - message: None, - percentage: None, - }, - ); - cx.notify(); + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.insert( + token, + LanguageServerProgress { + message: None, + percentage: None, + }, + ); + cx.notify(); + } } fn on_lsp_work_progress( @@ -1474,9 +1507,10 @@ impl Project { progress: LanguageServerProgress, cx: &mut ModelContext, ) { - self.pending_language_server_work - .insert((language_server_id, token), progress); - cx.notify(); + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.insert(token, progress); + cx.notify(); + } } fn on_lsp_work_end( @@ -1485,9 +1519,10 @@ impl Project { token: String, cx: &mut ModelContext, ) { - self.pending_language_server_work - .remove(&(language_server_id, token)); - cx.notify(); + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.remove(&token); + cx.notify(); + } } fn broadcast_language_server_update( @@ -1506,15 +1541,8 @@ impl Project { } } - pub fn pending_language_server_work( - &self, - ) -> impl Iterator { - self.pending_language_server_work.iter().filter_map( - |((language_server_id, token), progress)| { - let name = self.language_server_names.get(language_server_id)?; - Some((name.as_str(), token.as_str(), progress)) - }, - ) + pub fn language_server_statuses(&self) -> impl Iterator { + self.language_server_statuses.values() } pub fn update_diagnostics( @@ -3266,8 +3294,14 @@ impl Project { .server .ok_or_else(|| anyhow!("invalid server"))?; this.update(&mut cx, |this, cx| { - this.language_server_names - .insert(server.id as usize, server.name); + this.language_server_statuses.insert( + server.id as usize, + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + pending_diagnostic_updates: 0, + }, + ); cx.notify(); }); Ok(()) diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index e2976824b5..6907b02948 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,12 +1,13 @@ use crate::{ItemViewHandle, Settings, StatusItemView}; use futures::StreamExt; +use gpui::AppContext; use gpui::{ action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use postage::watch; -use project::Project; +use project::{LanguageServerProgress, Project}; use std::fmt::Write; use std::sync::Arc; @@ -81,6 +82,27 @@ impl LspStatus { self.failed.clear(); cx.notify(); } + + fn pending_language_server_work<'a>( + &self, + cx: &'a AppContext, + ) -> impl Iterator { + self.project + .read(cx) + .language_server_statuses() + .filter_map(|status| { + if status.pending_work.is_empty() { + None + } else { + Some( + status.pending_work.iter().map(|(token, progress)| { + (status.name.as_str(), token.as_str(), progress) + }), + ) + } + }) + .flatten() + } } impl Entity for LspStatus { @@ -95,7 +117,7 @@ impl View for LspStatus { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings_rx.borrow().theme; - let mut pending_work = self.project.read(cx).pending_language_server_work(); + let mut pending_work = self.pending_language_server_work(cx); if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { let mut message = lang_server_name.to_string(); From eddb089f2744f282c2e490c711fbf0fbf0532fc0 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 13:16:31 -0800 Subject: [PATCH 11/18] render character under block cursor --- crates/editor/src/editor.rs | 4 +-- crates/editor/src/element.rs | 61 +++++++++++++++++++++++++--------- crates/gpui/src/text_layout.rs | 20 +++++++++-- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 32a81f4f69..31a0387cce 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -1023,9 +1023,9 @@ impl Editor { cx.notify(); } - pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape) { + pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext) { self.cursor_shape = cursor_shape; - // TODO: Do we need to notify? + cx.notify(); } pub fn scroll_position(&self, cx: &mut ViewContext) -> Vector2F { diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 822d3ab085..caea726700 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -16,7 +16,7 @@ use gpui::{ PathBuilder, }, json::{self, ToJson}, - text_layout::{self, RunStyle, TextLayoutCache}, + text_layout::{self, Line, RunStyle, TextLayoutCache}, AppContext, Axis, Border, Element, ElementBox, Event, EventContext, LayoutContext, MutableAppContext, PaintContext, Quad, Scene, SizeConstraint, ViewContext, WeakViewHandle, }; @@ -347,7 +347,7 @@ impl EditorElement { let mut cursors = SmallVec::<[Cursor; 32]>::new(); for (replica_id, selections) in &layout.selections { - let style = style.replica_selection_style(*replica_id); + let selection_style = style.replica_selection_style(*replica_id); let corner_radius = 0.15 * layout.line_height; for selection in selections { @@ -355,7 +355,7 @@ impl EditorElement { selection.start..selection.end, start_row, end_row, - style.selection, + selection_style.selection, corner_radius, corner_radius * 2., layout, @@ -372,24 +372,49 @@ impl EditorElement { let cursor_row_layout = &layout.line_layouts[(cursor_position.row() - start_row) as usize]; let cursor_column = cursor_position.column() as usize; + let cursor_character_x = cursor_row_layout.x_for_index(cursor_column); - let mut character_width = + let mut block_width = cursor_row_layout.x_for_index(cursor_column + 1) - cursor_character_x; - // TODO: Is there a better option here for the character size - // at the end of the line? - // Default to 1/3 the line height - if character_width == 0.0 { - character_width = layout.line_height / 3.0; + if block_width == 0.0 { + block_width = layout.em_width; } + let block_text = + if matches!(self.cursor_shape, CursorShape::Block) { + layout.snapshot.chars_at(cursor_position).next().and_then( + |character| { + let font_id = + cursor_row_layout.font_for_index(cursor_column)?; + let text = character.to_string(); + + Some(cx.text_layout_cache.layout_str( + &text, + cursor_row_layout.font_size(), + &[( + text.len(), + RunStyle { + font_id, + color: style.background, + underline: None, + }, + )], + )) + }, + ) + } else { + None + }; + let x = cursor_character_x - scroll_left; let y = cursor_position.row() as f32 * layout.line_height - scroll_top; cursors.push(Cursor { - color: style.cursor, - character_width, + color: selection_style.cursor, + block_width, origin: content_origin + vec2f(x, y), line_height: layout.line_height, shape: self.cursor_shape, + block_text, }); } } @@ -1182,6 +1207,7 @@ fn layout_line( while !line.is_char_boundary(len) { len -= 1; } + line.truncate(len); } @@ -1242,16 +1268,17 @@ pub enum CursorShape { impl Default for CursorShape { fn default() -> Self { - CursorShape::Bar + CursorShape::Block } } struct Cursor { origin: Vector2F, - character_width: f32, + block_width: f32, line_height: f32, color: Color, shape: CursorShape, + block_text: Option, } impl Cursor { @@ -1259,11 +1286,11 @@ impl Cursor { let bounds = match self.shape { CursorShape::Bar => RectF::new(self.origin, vec2f(2.0, self.line_height)), CursorShape::Block => { - RectF::new(self.origin, vec2f(self.character_width, self.line_height)) + RectF::new(self.origin, vec2f(self.block_width, self.line_height)) } CursorShape::Underscore => RectF::new( self.origin + Vector2F::new(0.0, self.line_height - 2.0), - vec2f(self.character_width, 2.0), + vec2f(self.block_width, 2.0), ), }; @@ -1273,6 +1300,10 @@ impl Cursor { border: Border::new(0., Color::black()), corner_radius: 0., }); + + if let Some(block_text) = &self.block_text { + block_text.paint(self.origin, bounds, self.line_height, cx); + } } } diff --git a/crates/gpui/src/text_layout.rs b/crates/gpui/src/text_layout.rs index 6e371437bf..25f1cb82c0 100644 --- a/crates/gpui/src/text_layout.rs +++ b/crates/gpui/src/text_layout.rs @@ -186,7 +186,7 @@ pub struct Run { pub glyphs: Vec, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct Glyph { pub id: GlyphId, pub position: Vector2F, @@ -210,10 +210,14 @@ impl Line { self.layout.width } + pub fn font_size(&self) -> f32 { + self.layout.font_size + } + pub fn x_for_index(&self, index: usize) -> f32 { for run in &self.layout.runs { for glyph in &run.glyphs { - if glyph.index == index { + if glyph.index >= index { return glyph.position.x(); } } @@ -221,6 +225,18 @@ impl Line { self.layout.width } + pub fn font_for_index(&self, index: usize) -> Option { + for run in &self.layout.runs { + for glyph in &run.glyphs { + if glyph.index >= index { + return Some(run.font_id); + } + } + } + + None + } + pub fn index_for_x(&self, x: f32) -> Option { if x >= self.layout.width { None From 5b35c68d2e678beeeccd3969520540fe4feeca3e Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 13:20:45 -0800 Subject: [PATCH 12/18] Fix failing gpui test from missing cursor shape --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index caea726700..3238cf840f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1467,7 +1467,7 @@ mod tests { let (window_id, editor) = cx.add_window(Default::default(), |cx| { Editor::new(EditorMode::Full, buffer, None, settings.1, None, cx) }); - let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx)); + let element = EditorElement::new(editor.downgrade(), editor.read(cx).style(cx), CursorShape::Bar); let layouts = editor.update(cx, |editor, cx| { let snapshot = editor.snapshot(cx); From 5502c00d9a77bf4e0394a9e451bdfd43474eb5b2 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 13:28:13 -0800 Subject: [PATCH 13/18] swap default cursor shape back to bar --- crates/editor/src/element.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 3238cf840f..b15487b54c 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1268,7 +1268,7 @@ pub enum CursorShape { impl Default for CursorShape { fn default() -> Self { - CursorShape::Block + CursorShape::Bar } } From 81fc8122210c86e56d41423b24535382af7f77d4 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 20:03:01 -0800 Subject: [PATCH 14/18] Add global events to MutableAppContext and raise global event when new workspace is created --- crates/gpui/src/app.rs | 93 +++++++++++++++++++++++++++---- crates/workspace/src/workspace.rs | 21 ++++--- 2 files changed, 95 insertions(+), 19 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index e91963bfa6..d19ecce152 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -740,6 +740,7 @@ type ActionCallback = type GlobalActionCallback = dyn FnMut(&dyn AnyAction, &mut MutableAppContext); type SubscriptionCallback = Box bool>; +type GlobalSubscriptionCallback = Box; type ObservationCallback = Box bool>; type ReleaseObservationCallback = Box; @@ -757,6 +758,7 @@ pub struct MutableAppContext { next_subscription_id: usize, frame_count: usize, subscriptions: Arc>>>, + global_subscriptions: Arc>>>, observations: Arc>>>, release_observations: Arc>>>, presenters_and_platform_windows: @@ -804,6 +806,7 @@ impl MutableAppContext { next_subscription_id: 0, frame_count: 0, subscriptions: Default::default(), + global_subscriptions: Default::default(), observations: Default::default(), release_observations: Default::default(), presenters_and_platform_windows: HashMap::new(), @@ -1062,6 +1065,12 @@ impl MutableAppContext { self.foreground_platform.prompt_for_new_path(directory) } + pub fn emit_global(&mut self, payload: E) { + self.pending_effects.push_back(Effect::GlobalEvent { + payload: Box::new(payload), + }); + } + pub fn subscribe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -1075,6 +1084,31 @@ impl MutableAppContext { }) } + pub fn global_subscribe(&mut self, mut callback: F) -> Subscription + where + E: Any + Copy, + F: 'static + FnMut(&E, &mut Self), + { + let id = post_inc(&mut self.next_subscription_id); + let type_id = TypeId::of::(); + self.global_subscriptions + .lock() + .entry(type_id) + .or_default() + .insert( + id, + Box::new(move |payload, cx| { + let payload = payload.downcast_ref().expect("downcast is type safe"); + callback(payload, cx) + })); + Subscription::GlobalSubscription { + id, + type_id, + subscriptions: Some(Arc::downgrade(&self.global_subscriptions)) + } + } + + pub fn observe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -1573,6 +1607,7 @@ impl MutableAppContext { if let Some(effect) = self.pending_effects.pop_front() { match effect { Effect::Event { entity_id, payload } => self.emit_event(entity_id, payload), + Effect::GlobalEvent { payload } => self.emit_global_event(payload), Effect::ModelNotification { model_id } => { self.notify_model_observers(model_id) } @@ -1700,6 +1735,16 @@ impl MutableAppContext { } } + fn emit_global_event(&mut self, payload: Box) { + let type_id = (&*payload).type_id(); + let callbacks = self.global_subscriptions.lock().remove(&type_id); + if let Some(callbacks) = callbacks { + for (_, mut callback) in callbacks { + callback(payload.as_ref(), self) + } + } + } + fn notify_model_observers(&mut self, observed_id: usize) { let callbacks = self.observations.lock().remove(&observed_id); if let Some(callbacks) = callbacks { @@ -2071,6 +2116,9 @@ pub enum Effect { entity_id: usize, payload: Box, }, + GlobalEvent { + payload: Box, + }, ModelNotification { model_id: usize, }, @@ -2104,6 +2152,10 @@ impl Debug for Effect { .debug_struct("Effect::Event") .field("entity_id", entity_id) .finish(), + Effect::GlobalEvent { payload, .. } => f + .debug_struct("Effect::GlobalEvent") + .field("type_id", &(&*payload).type_id()) + .finish(), Effect::ModelNotification { model_id } => f .debug_struct("Effect::ModelNotification") .field("model_id", model_id) @@ -3762,6 +3814,11 @@ pub enum Subscription { entity_id: usize, subscriptions: Option>>>>, }, + GlobalSubscription { + id: usize, + type_id: TypeId, + subscriptions: Option>>>>, + }, Observation { id: usize, entity_id: usize, @@ -3781,6 +3838,9 @@ impl Subscription { Subscription::Subscription { subscriptions, .. } => { subscriptions.take(); } + Subscription::GlobalSubscription { subscriptions, .. } => { + subscriptions.take(); + } Subscription::Observation { observations, .. } => { observations.take(); } @@ -3794,6 +3854,28 @@ impl Subscription { impl Drop for Subscription { fn drop(&mut self) { match self { + Subscription::Subscription { + id, + entity_id, + subscriptions, + } => { + if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { + if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) { + subscriptions.remove(id); + } + } + } + Subscription::GlobalSubscription { + id, + type_id, + subscriptions, + } => { + if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { + if let Some(subscriptions) = subscriptions.lock().get_mut(type_id) { + subscriptions.remove(id); + } + } + } Subscription::Observation { id, entity_id, @@ -3816,17 +3898,6 @@ impl Drop for Subscription { } } } - Subscription::Subscription { - id, - entity_id, - subscriptions, - } => { - if let Some(subscriptions) = subscriptions.as_ref().and_then(Weak::upgrade) { - if let Some(subscriptions) = subscriptions.lock().get_mut(entity_id) { - subscriptions.remove(id); - } - } - } } } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f06a244c05..5680ad4cdf 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1511,6 +1511,8 @@ fn open(action: &Open, cx: &mut MutableAppContext) { .detach(); } +pub struct WorkspaceBuilt(WeakViewHandle); + pub fn open_paths( abs_paths: &[PathBuf], app_state: &Arc, @@ -1537,7 +1539,7 @@ pub fn open_paths( } let workspace = existing.unwrap_or_else(|| { - cx.add_window((app_state.build_window_options)(), |cx| { + let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { let project = Project::local( app_state.client.clone(), app_state.user_store.clone(), @@ -1546,8 +1548,9 @@ pub fn open_paths( cx, ); (app_state.build_workspace)(project, &app_state, cx) - }) - .1 + }); + cx.emit_global(WorkspaceBuilt(workspace.downgrade())); + workspace }); let task = workspace.update(cx, |workspace, cx| workspace.open_paths(abs_paths, cx)); @@ -1581,12 +1584,13 @@ pub fn join_project( &mut cx, ) .await?; - let (_, workspace) = cx.update(|cx| { - cx.add_window((app_state.build_window_options)(), |cx| { + Ok(cx.update(|cx| { + let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { (app_state.build_workspace)(project, &app_state, cx) - }) - }); - Ok(workspace) + }); + cx.emit_global(WorkspaceBuilt(workspace.downgrade())); + workspace + })) }) } @@ -1601,5 +1605,6 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { ); (app_state.build_workspace)(project, &app_state, cx) }); + cx.emit_global(WorkspaceBuilt(workspace.downgrade())); cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone())); } From 5f62f69907676e9d52f58a69c0dc38a532ccb15a Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Thu, 10 Mar 2022 20:04:16 -0800 Subject: [PATCH 15/18] Add unwrap check if buffer_line not available --- crates/editor/src/editor.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 4d49fb7149..0544355359 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -2639,21 +2639,22 @@ impl Editor { for selection in &mut selections { if selection.is_empty() { let old_head = selection.head(); - let (buffer, line_buffer_range) = display_map - .buffer_snapshot - .buffer_line_for_row(old_head.row) - .unwrap(); - let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); let mut new_head = movement::left(&display_map, old_head.to_display_point(&display_map)) .unwrap() .to_point(&display_map); - if old_head.column <= indent_column && old_head.column > 0 { - let indent = buffer.indent_size(); - new_head = cmp::min( - new_head, - Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), - ); + if let Some((buffer, line_buffer_range)) = display_map + .buffer_snapshot + .buffer_line_for_row(old_head.row) + { + let indent_column = buffer.indent_column_for_line(line_buffer_range.start.row); + if old_head.column <= indent_column && old_head.column > 0 { + let indent = buffer.indent_size(); + new_head = cmp::min( + new_head, + Point::new(old_head.row, ((old_head.column - 1) / indent) * indent), + ); + } } selection.set_head(new_head); From 7a454003fe18722f545402334d8632ab66ade6b7 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 11 Mar 2022 09:59:13 +0100 Subject: [PATCH 16/18] Show the last in-progress task from language servers --- Cargo.lock | 1 + crates/project/src/project.rs | 10 ++++++++-- crates/workspace/Cargo.toml | 1 + crates/workspace/src/lsp_status.rs | 15 ++++++++++----- 4 files changed, 20 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7aafce3bbb..499acae085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5852,6 +5852,7 @@ dependencies = [ "postage", "project", "serde_json", + "smallvec", "theme", "util", ] diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index dd4a17c13a..430c6875d0 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -136,10 +136,11 @@ pub struct LanguageServerStatus { pending_diagnostic_updates: isize, } -#[derive(Clone, Default)] +#[derive(Clone, Debug)] pub struct LanguageServerProgress { pub message: Option, pub percentage: Option, + pub last_update_at: Instant, } #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] @@ -1253,6 +1254,7 @@ impl Project { percentage: report .percentage .map(|p| p as usize), + last_update_at: Instant::now(), }, }) .ok(); @@ -1494,6 +1496,7 @@ impl Project { LanguageServerProgress { message: None, percentage: None, + last_update_at: Instant::now(), }, ); cx.notify(); @@ -1541,7 +1544,9 @@ impl Project { } } - pub fn language_server_statuses(&self) -> impl Iterator { + pub fn language_server_statuses( + &self, + ) -> impl DoubleEndedIterator { self.language_server_statuses.values() } @@ -3332,6 +3337,7 @@ impl Project { LanguageServerProgress { message: payload.message, percentage: payload.percentage.map(|p| p as usize), + last_update_at: Instant::now(), }, cx, ); diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d83cbf29d4..e92c4bf186 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -24,6 +24,7 @@ futures = "0.3" log = "0.4" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } +smallvec = { version = "1.6", features = ["union"] } [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index 6907b02948..43b58bd02b 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -8,6 +8,8 @@ use gpui::{ use language::{LanguageRegistry, LanguageServerBinaryStatus}; use postage::watch; use project::{LanguageServerProgress, Project}; +use smallvec::SmallVec; +use std::cmp::Reverse; use std::fmt::Write; use std::sync::Arc; @@ -90,15 +92,18 @@ impl LspStatus { self.project .read(cx) .language_server_statuses() + .rev() .filter_map(|status| { if status.pending_work.is_empty() { None } else { - Some( - status.pending_work.iter().map(|(token, progress)| { - (status.name.as_str(), token.as_str(), progress) - }), - ) + let mut pending_work = status + .pending_work + .iter() + .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress)) + .collect::>(); + pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at)); + Some(pending_work) } }) .flatten() From 18b1e9d35f789a26f5094ea5df855755c593ccde Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 11 Mar 2022 10:02:37 +0100 Subject: [PATCH 17/18] Don't starve main thread when lots of messages/events arrive at once --- crates/client/src/client.rs | 3 +++ crates/project/src/project.rs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 62d2c6fb31..59110f73c6 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -631,6 +631,9 @@ impl Client { } else { log::info!("unhandled message {}", type_name); } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; } } }) diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 430c6875d0..2f84f702be 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -1278,6 +1278,9 @@ impl Project { this.update(&mut cx, |this, cx| { this.on_lsp_event(server_id, event, &language, cx) }); + + // Don't starve the main thread when lots of events arrive all at once. + smol::future::yield_now().await; } Some(()) } From 144591d63969750721518480d8ced0b6822acbf9 Mon Sep 17 00:00:00 2001 From: Keith Simmons Date: Fri, 11 Mar 2022 11:25:36 -0800 Subject: [PATCH 18/18] Minor renames for clarity --- crates/gpui/src/app.rs | 15 ++++++++------- crates/workspace/src/workspace.rs | 8 ++++---- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index d19ecce152..109e52ae0b 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1084,9 +1084,9 @@ impl MutableAppContext { }) } - pub fn global_subscribe(&mut self, mut callback: F) -> Subscription + pub fn subscribe_global(&mut self, mut callback: F) -> Subscription where - E: Any + Copy, + E: Any, F: 'static + FnMut(&E, &mut Self), { let id = post_inc(&mut self.next_subscription_id); @@ -1096,19 +1096,19 @@ impl MutableAppContext { .entry(type_id) .or_default() .insert( - id, + id, Box::new(move |payload, cx| { let payload = payload.downcast_ref().expect("downcast is type safe"); callback(payload, cx) - })); + }), + ); Subscription::GlobalSubscription { id, type_id, - subscriptions: Some(Arc::downgrade(&self.global_subscriptions)) + subscriptions: Some(Arc::downgrade(&self.global_subscriptions)), } } - pub fn observe(&mut self, handle: &H, mut callback: F) -> Subscription where E: Entity, @@ -3817,7 +3817,8 @@ pub enum Subscription { GlobalSubscription { id: usize, type_id: TypeId, - subscriptions: Option>>>>, + subscriptions: + Option>>>>, }, Observation { id: usize, diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5680ad4cdf..81633e3f2c 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1511,7 +1511,7 @@ fn open(action: &Open, cx: &mut MutableAppContext) { .detach(); } -pub struct WorkspaceBuilt(WeakViewHandle); +pub struct WorkspaceCreated(WeakViewHandle); pub fn open_paths( abs_paths: &[PathBuf], @@ -1549,7 +1549,7 @@ pub fn open_paths( ); (app_state.build_workspace)(project, &app_state, cx) }); - cx.emit_global(WorkspaceBuilt(workspace.downgrade())); + cx.emit_global(WorkspaceCreated(workspace.downgrade())); workspace }); @@ -1588,7 +1588,7 @@ pub fn join_project( let (_, workspace) = cx.add_window((app_state.build_window_options)(), |cx| { (app_state.build_workspace)(project, &app_state, cx) }); - cx.emit_global(WorkspaceBuilt(workspace.downgrade())); + cx.emit_global(WorkspaceCreated(workspace.downgrade())); workspace })) }) @@ -1605,6 +1605,6 @@ fn open_new(app_state: &Arc, cx: &mut MutableAppContext) { ); (app_state.build_workspace)(project, &app_state, cx) }); - cx.emit_global(WorkspaceBuilt(workspace.downgrade())); + cx.emit_global(WorkspaceCreated(workspace.downgrade())); cx.dispatch_action(window_id, vec![workspace.id()], &OpenNew(app_state.clone())); }