diff --git a/Cargo.lock b/Cargo.lock index cb0d54022f2d4233285fd66735a21625064cb38f..bd0a86d16128c219047e2ea3b47548534192d1a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1753,7 +1753,7 @@ dependencies = [ [[package]] name = "collab" -version = "0.31.0" +version = "0.32.0" dependencies = [ "anyhow", "async-trait", @@ -4990,6 +4990,30 @@ dependencies = [ "workspace", ] +[[package]] +name = "language_tools2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "collections", + "editor2", + "env_logger 0.9.3", + "futures 0.3.28", + "gpui2", + "language2", + "lsp2", + "project2", + "serde", + "settings2", + "theme2", + "tree-sitter", + "ui2", + "unindent", + "util", + "workspace2", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -7457,7 +7481,6 @@ dependencies = [ name = "recent_projects2" version = "0.1.0" dependencies = [ - "db", "editor2", "futures 0.3.28", "fuzzy2", @@ -11988,7 +12011,7 @@ dependencies = [ [[package]] name = "zed" -version = "0.118.0" +version = "0.119.0" dependencies = [ "activity_indicator", "ai", @@ -12174,6 +12197,7 @@ dependencies = [ "journal2", "language2", "language_selector2", + "language_tools2", "lazy_static", "libc", "log", diff --git a/Cargo.toml b/Cargo.toml index 3b453527b89dca285b303c9ddd89d5bcc6792310..42432a8a2a0f6fa3d713769f7b7c14f962d02520 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,7 @@ members = [ "crates/language_selector", "crates/language_selector2", "crates/language_tools", + "crates/language_tools2", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", diff --git a/crates/activity_indicator2/src/activity_indicator.rs b/crates/activity_indicator2/src/activity_indicator.rs index e4a5b01ba6bdb276f00d20f3f7748b771c2b2ed8..f83517e0618316a5407f5ab6a9bc3cabc653033a 100644 --- a/crates/activity_indicator2/src/activity_indicator.rs +++ b/crates/activity_indicator2/src/activity_indicator.rs @@ -10,7 +10,7 @@ use language::{LanguageRegistry, LanguageServerBinaryStatus}; use project::{LanguageServerProgress, Project}; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc}; -use ui::h_stack; +use ui::{h_stack, Label}; use util::ResultExt; use workspace::{item::ItemHandle, StatusItemView, Workspace}; @@ -324,7 +324,7 @@ impl Render for ActivityIndicator { result .children(content.icon.map(|icon| svg().path(icon))) - .child(SharedString::from(content.message)) + .child(Label::new(SharedString::from(content.message))) } } diff --git a/crates/collab/Cargo.toml b/crates/collab/Cargo.toml index fd5c183d2af600fd7ea385072bfc1bf8fae19557..7bab81c4ed5e8b5b70a7170840d6d1d77fcaa7cc 100644 --- a/crates/collab/Cargo.toml +++ b/crates/collab/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] default-run = "collab" edition = "2021" name = "collab" -version = "0.31.0" +version = "0.32.0" publish = false [[bin]] diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 35e2f8d7ed210dc3cfb2fbe59d1d399a3f2683b9..20edd9be56636259700bef95e9c83d1a74f017d2 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -1624,40 +1624,41 @@ impl CollabPanel { } fn render_signed_out(&mut self, cx: &mut ViewContext) -> Div { - v_stack() - .items_center() - .child(v_stack().gap_6().p_4() - .child( - Label::new("Work with your team in realtime with collaborative editing, voice, shared notes and more.") - ) - .child(v_stack().gap_2() + let collab_blurb = "Work with your team in realtime with collaborative editing, voice, shared notes and more."; - .child( - Button::new("sign_in", "Sign in") - .icon_color(Color::Muted) - .icon(Icon::Github) - .icon_position(IconPosition::Start) - .style(ButtonStyle::Filled) - .full_width() - .on_click(cx.listener( - |this, _, cx| { - let client = this.client.clone(); - cx.spawn(|_, mut cx| async move { - client - .authenticate_and_connect(true, &cx) - .await - .notify_async_err(&mut cx); - }) - .detach() - }, - ))) - .child( - div().flex().w_full().items_center().child( - Label::new("Sign in to enable collaboration.") - .color(Color::Muted) - .size(LabelSize::Small) - )), - )) + v_stack() + .gap_6() + .p_4() + .child(Label::new(collab_blurb)) + .child( + v_stack() + .gap_2() + .child( + Button::new("sign_in", "Sign in") + .icon_color(Color::Muted) + .icon(Icon::Github) + .icon_position(IconPosition::Start) + .style(ButtonStyle::Filled) + .full_width() + .on_click(cx.listener(|this, _, cx| { + let client = this.client.clone(); + cx.spawn(|_, mut cx| async move { + client + .authenticate_and_connect(true, &cx) + .await + .notify_async_err(&mut cx); + }) + .detach() + })), + ) + .child( + div().flex().w_full().items_center().child( + Label::new("Sign in to enable collaboration.") + .color(Color::Muted) + .size(LabelSize::Small), + ), + ), + ) } fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index ae664859192e2bbb635d1f4552f604106bc61b54..24e510ae8d51175b7b28de32a2807c44927e4761 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -68,7 +68,6 @@ impl Render for CollabTitlebarItem { h_stack() .id("titlebar") - .z_index(160) // todo!("z-index") .justify_between() .w_full() .h(rems(1.75)) diff --git a/crates/collab_ui2/src/notification_panel.rs b/crates/collab_ui2/src/notification_panel.rs index 35288e810d645bf7ea2c158df071cc51fde4efc1..20951b34dbc34fbc007badbe6226c0f6f80bdfad 100644 --- a/crates/collab_ui2/src/notification_panel.rs +++ b/crates/collab_ui2/src/notification_panel.rs @@ -6,11 +6,11 @@ use collections::HashMap; use db::kvp::KEY_VALUE_STORE; use futures::StreamExt; use gpui::{ - actions, div, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, CursorStyle, - DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - IntoElement, ListAlignment, ListScrollEvent, ListState, Model, ParentElement, Render, Stateful, - StatefulInteractiveElement, Styled, Task, View, ViewContext, VisualContext, WeakView, - WindowContext, + actions, div, img, list, px, serde_json, AnyElement, AppContext, AsyncWindowContext, + CursorStyle, DismissEvent, Div, Element, EventEmitter, FocusHandle, FocusableView, + InteractiveElement, IntoElement, ListAlignment, ListScrollEvent, ListState, Model, + ParentElement, Render, Stateful, StatefulInteractiveElement, Styled, Task, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use notifications::{NotificationEntry, NotificationEvent, NotificationStore}; use project::Fs; @@ -19,7 +19,7 @@ use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use std::{sync::Arc, time::Duration}; use time::{OffsetDateTime, UtcOffset}; -use ui::{h_stack, v_stack, Avatar, Button, Clickable, Icon, IconButton, IconElement, Label}; +use ui::{h_stack, prelude::*, v_stack, Avatar, Button, Icon, IconButton, IconElement, Label}; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -229,69 +229,88 @@ impl NotificationPanel { Some( div() .id(ix) + .flex() + .flex_row() + .size_full() + .px_2() + .py_1() + .gap_2() + .when(can_navigate, |el| { + el.cursor(CursorStyle::PointingHand).on_click({ + let notification = notification.clone(); + cx.listener(move |this, _, cx| { + this.did_click_notification(¬ification, cx) + }) + }) + }) + .children(actor.map(|actor| { + img(actor.avatar_uri.clone()) + .flex_none() + .w_8() + .h_8() + .rounded_full() + })) .child( - h_stack() - .children(actor.map(|actor| Avatar::new(actor.avatar_uri.clone()))) + v_stack() + .gap_1() + .size_full() + .overflow_hidden() + .child(Label::new(text.clone())) .child( - v_stack().child(Label::new(text)).child( - h_stack() - .child(Label::new(format_timestamp( + h_stack() + .child( + Label::new(format_timestamp( timestamp, now, self.local_timezone, - ))) - .children(if let Some(is_accepted) = response { - Some(div().child(Label::new(if is_accepted { + )) + .color(Color::Muted), + ) + .children(if let Some(is_accepted) = response { + Some(div().flex().flex_grow().justify_end().child(Label::new( + if is_accepted { "You accepted" } else { "You declined" - }))) - } else if needs_response { - Some( - h_stack() - .child(Button::new("decline", "Decline").on_click( - { - let notification = notification.clone(); - let view = cx.view().clone(); - move |_, cx| { - view.update(cx, |this, cx| { - this.respond_to_notification( - notification.clone(), - false, - cx, - ) - }); - } - }, - )) - .child(Button::new("accept", "Accept").on_click({ - let notification = notification.clone(); - let view = cx.view().clone(); - move |_, cx| { - view.update(cx, |this, cx| { - this.respond_to_notification( - notification.clone(), - true, - cx, - ) - }); - } - })), - ) - } else { - None - }), - ), + }, + ))) + } else if needs_response { + Some( + h_stack() + .flex_grow() + .justify_end() + .child(Button::new("decline", "Decline").on_click({ + let notification = notification.clone(); + let view = cx.view().clone(); + move |_, cx| { + view.update(cx, |this, cx| { + this.respond_to_notification( + notification.clone(), + false, + cx, + ) + }); + } + })) + .child(Button::new("accept", "Accept").on_click({ + let notification = notification.clone(); + let view = cx.view().clone(); + move |_, cx| { + view.update(cx, |this, cx| { + this.respond_to_notification( + notification.clone(), + true, + cx, + ) + }); + } + })), + ) + } else { + None + }), ), ) - .when(can_navigate, |el| { - el.cursor(CursorStyle::PointingHand).on_click({ - let notification = notification.clone(); - cx.listener(move |this, _, cx| { - this.did_click_notification(¬ification, cx) - }) - }) - }) .into_any(), ) } @@ -439,28 +458,6 @@ impl NotificationPanel { false } - fn render_sign_in_prompt(&self) -> AnyElement { - Button::new( - "sign_in_prompt_button", - "Sign in to view your notifications", - ) - .on_click({ - let client = self.client.clone(); - move |_, cx| { - let client = client.clone(); - cx.spawn(move |cx| async move { - client.authenticate_and_connect(true, &cx).log_err().await; - }) - .detach() - } - }) - .into_any_element() - } - - fn render_empty_state(&self) -> AnyElement { - Label::new("You have no notifications").into_any_element() - } - fn on_notification_event( &mut self, _: Model, @@ -543,25 +540,72 @@ impl NotificationPanel { } impl Render for NotificationPanel { - type Element = AnyElement; + type Element = Div; - fn render(&mut self, _: &mut ViewContext) -> AnyElement { - if self.client.user_id().is_none() { - self.render_sign_in_prompt() - } else if self.notification_list.item_count() == 0 { - self.render_empty_state() - } else { - v_stack() - .bg(gpui::red()) - .child( - h_stack() - .child(Label::new("Notifications")) - .child(IconElement::new(Icon::Envelope)), - ) - .child(list(self.notification_list.clone()).size_full()) - .size_full() - .into_any_element() - } + fn render(&mut self, cx: &mut ViewContext) -> Div { + v_stack() + .size_full() + .child( + h_stack() + .justify_between() + .px_2() + .py_1() + // Match the height of the tab bar so they line up. + .h(rems(ui::Tab::HEIGHT_IN_REMS)) + .border_b_1() + .border_color(cx.theme().colors().border) + .child(Label::new("Notifications")) + .child(IconElement::new(Icon::Envelope)), + ) + .map(|this| { + if self.client.user_id().is_none() { + this.child( + v_stack() + .gap_2() + .p_4() + .child( + Button::new("sign_in_prompt_button", "Sign in") + .icon_color(Color::Muted) + .icon(Icon::Github) + .icon_position(IconPosition::Start) + .style(ButtonStyle::Filled) + .full_width() + .on_click({ + let client = self.client.clone(); + move |_, cx| { + let client = client.clone(); + cx.spawn(move |cx| async move { + client + .authenticate_and_connect(true, &cx) + .log_err() + .await; + }) + .detach() + } + }), + ) + .child( + div().flex().w_full().items_center().child( + Label::new("Sign in to view notifications.") + .color(Color::Muted) + .size(LabelSize::Small), + ), + ), + ) + } else if self.notification_list.item_count() == 0 { + this.child( + v_stack().p_4().child( + div().flex().w_full().items_center().child( + Label::new("You have no notifications.") + .color(Color::Muted) + .size(LabelSize::Small), + ), + ), + ) + } else { + this.child(list(self.notification_list.clone()).size_full()) + } + }) } } diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index 53ff51d1cd8bd641cd46b90a508616968fa4de80..d5bdf4ff446887cd69cfe96676aec6964b9e0c24 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -2,12 +2,14 @@ use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; use futures::StreamExt; use gpui::{ - div, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, Styled, ViewContext, + img, px, AppContext, Div, ParentElement, Render, RenderOnce, Styled, ViewContext, VisualContext as _, WindowHandle, }; +use settings::Settings; use std::sync::{Arc, Weak}; +use theme::ThemeSettings; use ui::prelude::*; -use ui::{h_stack, v_stack, Avatar, Button, Label}; +use ui::{h_stack, v_stack, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -112,36 +114,52 @@ impl IncomingCallNotification { state: Arc::new(IncomingCallNotificationState::new(call, app_state)), } } - fn render_caller(&self, cx: &mut ViewContext) -> impl Element { - h_stack() - .child(Avatar::new(self.state.call.calling_user.avatar_uri.clone())) - .child( - v_stack() - .child(Label::new(format!( - "{} is sharing a project in Zed", - self.state.call.calling_user.github_login - ))) - .child(self.render_buttons(cx)), - ) - } - - fn render_buttons(&self, cx: &mut ViewContext) -> impl Element { - h_stack() - .child(Button::new("accept", "Accept").render(cx).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(true, cx) - })) - .child(Button::new("decline", "Decline").render(cx).on_click({ - let state = self.state.clone(); - move |_, cx| state.respond(false, cx) - })) - } } impl Render for IncomingCallNotification { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div().bg(red()).flex_none().child(self.render_caller(cx)) + // TODO: Is there a better place for us to initialize the font? + let (ui_font, ui_font_size) = { + let theme_settings = ThemeSettings::get_global(cx); + ( + theme_settings.ui_font.family.clone(), + theme_settings.ui_font_size.clone(), + ) + }; + + cx.set_rem_size(ui_font_size); + + h_stack() + .font(ui_font) + .text_ui() + .justify_between() + .size_full() + .overflow_hidden() + .elevation_3(cx) + .p_2() + .gap_2() + .child( + img(self.state.call.calling_user.avatar_uri.clone()) + .w_12() + .h_12() + .rounded_full(), + ) + .child(v_stack().overflow_hidden().child(Label::new(format!( + "{} is sharing a project in Zed", + self.state.call.calling_user.github_login + )))) + .child( + v_stack() + .child(Button::new("accept", "Accept").render(cx).on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + })) + .child(Button::new("decline", "Decline").render(cx).on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) + })), + ) } } diff --git a/crates/collab_ui2/src/notifications/project_shared_notification.rs b/crates/collab_ui2/src/notifications/project_shared_notification.rs index 2dc0dee6f47e8a9dfc4dcf9fef957cc75147a3d2..707e961423472ddb8b032a253bb20ff66f2adbaa 100644 --- a/crates/collab_ui2/src/notifications/project_shared_notification.rs +++ b/crates/collab_ui2/src/notifications/project_shared_notification.rs @@ -23,7 +23,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { } => { let window_size = Size { width: px(400.), - height: px(96.), + height: px(72.), }; for screen in cx.displays() { @@ -139,35 +139,33 @@ impl Render for ProjectSharedNotification { .text_ui() .justify_between() .size_full() + .overflow_hidden() .elevation_3(cx) .p_2() .gap_2() .child( - h_stack() - .gap_2() - .child( - img(self.owner.avatar_uri.clone()) - .w_16() - .h_16() - .rounded_full(), - ) - .child( - v_stack() - .child(Label::new(self.owner.github_login.clone())) - .child(Label::new(format!( - "is sharing a project in Zed{}", - if self.worktree_root_names.is_empty() { - "" - } else { - ":" - } - ))) - .children(if self.worktree_root_names.is_empty() { - None - } else { - Some(Label::new(self.worktree_root_names.join(", "))) - }), - ), + img(self.owner.avatar_uri.clone()) + .w_12() + .h_12() + .rounded_full(), + ) + .child( + v_stack() + .overflow_hidden() + .child(Label::new(self.owner.github_login.clone())) + .child(Label::new(format!( + "is sharing a project in Zed{}", + if self.worktree_root_names.is_empty() { + "" + } else { + ":" + } + ))) + .children(if self.worktree_root_names.is_empty() { + None + } else { + Some(Label::new(self.worktree_root_names.join(", "))) + }), ) .child( v_stack() diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs index 92b0641deae4d049fda3d968cf88b28568b50d41..14ebe0c5166e07a641e6e94bec54025eaed6c0b7 100644 --- a/crates/diagnostics2/src/items.rs +++ b/crates/diagnostics2/src/items.rs @@ -1,16 +1,15 @@ use collections::HashSet; -use editor::{Editor, GoToDiagnostic}; +use editor::Editor; use gpui::{ - rems, Div, EventEmitter, InteractiveElement, ParentElement, Render, Stateful, - StatefulInteractiveElement, Styled, Subscription, View, ViewContext, WeakView, + rems, Div, EventEmitter, IntoElement, ParentElement, Render, Styled, Subscription, View, + ViewContext, WeakView, }; use language::Diagnostic; use lsp::LanguageServerId; -use theme::ActiveTheme; -use ui::{h_stack, Color, Icon, IconElement, Label, Tooltip}; +use ui::{h_stack, prelude::*, Button, ButtonLike, Color, Icon, IconElement, Label, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; -use crate::ProjectDiagnosticsEditor; +use crate::{Deploy, ProjectDiagnosticsEditor}; pub struct DiagnosticIndicator { summary: project::DiagnosticSummary, @@ -22,7 +21,7 @@ pub struct DiagnosticIndicator { } impl Render for DiagnosticIndicator { - type Element = Stateful
; + type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let diagnostic_indicator = match (self.summary.error_count, self.summary.warning_count) { @@ -43,26 +42,40 @@ impl Render for DiagnosticIndicator { .child(Label::new(warning_count.to_string())), }; + let status = if !self.in_progress_checks.is_empty() { + Some(Label::new("Checking…").into_any_element()) + } else if let Some(diagnostic) = &self.current_diagnostic { + let message = diagnostic.message.split('\n').next().unwrap().to_string(); + Some( + Button::new("diagnostic_message", message) + .tooltip(|cx| { + Tooltip::for_action("Next Diagnostic", &editor::GoToDiagnostic, cx) + }) + .on_click(cx.listener(|this, _, cx| { + this.go_to_next_diagnostic(cx); + })) + .into_any_element(), + ) + } else { + None + }; + h_stack() - .id("diagnostic-indicator") - .on_action(cx.listener(Self::go_to_next_diagnostic)) - .rounded_md() - .flex_none() .h(rems(1.375)) - .px_1() - .cursor_pointer() - .bg(cx.theme().colors().ghost_element_background) - .hover(|style| style.bg(cx.theme().colors().ghost_element_hover)) - .active(|style| style.bg(cx.theme().colors().ghost_element_active)) - .tooltip(|cx| Tooltip::text("Project Diagnostics", cx)) - .on_click(cx.listener(|this, _, cx| { - if let Some(workspace) = this.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx) - }) - } - })) - .child(diagnostic_indicator) + .gap_2() + .child( + ButtonLike::new("diagnostic-indicator") + .child(diagnostic_indicator) + .tooltip(|cx| Tooltip::for_action("Project Diagnostics", &Deploy, cx)) + .on_click(cx.listener(|this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx) + }) + } + })), + ) + .children(status) } } @@ -104,7 +117,7 @@ impl DiagnosticIndicator { } } - fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { + fn go_to_next_diagnostic(&mut self, cx: &mut ViewContext) { if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) { editor.update(cx, |editor, cx| { editor.go_to_diagnostic_impl(editor::Direction::Next, cx); diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 58f8e857a9a167219b5e1cdeec5a2bbb1b5b62f9..81158c5351052d450a0b329e03a09bc1dc305eac 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -1652,14 +1652,11 @@ impl Editor { Self::new(EditorMode::SingleLine, buffer, None, cx) } - // pub fn multi_line( - // field_editor_style: Option>, - // cx: &mut ViewContext, - // ) -> Self { - // let buffer = cx.build_model(|cx| Buffer::new(0, cx.model_id() as u64, String::new())); - // let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); - // Self::new(EditorMode::Full, buffer, None, field_editor_style, cx) - // } + pub fn multi_line(cx: &mut ViewContext) -> Self { + let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); + let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); + Self::new(EditorMode::Full, buffer, None, cx) + } pub fn auto_height(max_lines: usize, cx: &mut ViewContext) -> Self { let buffer = cx.build_model(|cx| Buffer::new(0, cx.entity_id().as_u64(), String::new())); diff --git a/crates/gpui2/src/arena.rs b/crates/gpui2/src/arena.rs index c99f09d29f46e33117698cfc8ffbacaa239c9e9e..ef66188a0ebcf7b6b4c4175bec2c7a571478f650 100644 --- a/crates/gpui2/src/arena.rs +++ b/crates/gpui2/src/arena.rs @@ -2,13 +2,13 @@ use std::{ alloc, cell::Cell, ops::{Deref, DerefMut}, - ptr::{self, NonNull}, + ptr, rc::Rc, }; struct ArenaElement { - value: NonNull, - drop: unsafe fn(NonNull), + value: *mut u8, + drop: unsafe fn(*mut u8), } impl Drop for ArenaElement { @@ -21,8 +21,9 @@ impl Drop for ArenaElement { } pub struct Arena { - start: NonNull, - offset: usize, + start: *mut u8, + end: *mut u8, + offset: *mut u8, elements: Vec, valid: Rc>, } @@ -31,10 +32,12 @@ impl Arena { pub fn new(size_in_bytes: usize) -> Self { unsafe { let layout = alloc::Layout::from_size_align(size_in_bytes, 1).unwrap(); - let ptr = alloc::alloc(layout); + let start = alloc::alloc(layout); + let end = start.add(size_in_bytes); Self { - start: NonNull::new_unchecked(ptr), - offset: 0, + start, + end, + offset: start, elements: Vec::new(), valid: Rc::new(Cell::new(true)), } @@ -45,7 +48,7 @@ impl Arena { self.valid.set(false); self.valid = Rc::new(Cell::new(true)); self.elements.clear(); - self.offset = 0; + self.offset = self.start; } #[inline(always)] @@ -58,24 +61,28 @@ impl Arena { ptr::write(ptr, f()); } - unsafe fn drop(ptr: NonNull) { - std::ptr::drop_in_place(ptr.cast::().as_ptr()); + unsafe fn drop(ptr: *mut u8) { + std::ptr::drop_in_place(ptr.cast::()); } unsafe { let layout = alloc::Layout::new::().pad_to_align(); - let ptr = NonNull::new_unchecked(self.start.as_ptr().add(self.offset).cast::()); - inner_writer(ptr.as_ptr(), f); + let next_offset = self.offset.add(layout.size()); + assert!(next_offset <= self.end); + let result = ArenaRef { + ptr: self.offset.cast(), + valid: self.valid.clone(), + }; + + inner_writer(result.ptr, f); self.elements.push(ArenaElement { - value: ptr.cast(), + value: self.offset, drop: drop::, }); - self.offset += layout.size(); - ArenaRef { - ptr, - valid: self.valid.clone(), - } + self.offset = next_offset; + + result } } } @@ -87,24 +94,15 @@ impl Drop for Arena { } pub struct ArenaRef { - ptr: NonNull, + ptr: *mut T, valid: Rc>, } -impl Clone for ArenaRef { - fn clone(&self) -> Self { - Self { - ptr: self.ptr, - valid: self.valid.clone(), - } - } -} - impl ArenaRef { #[inline(always)] pub fn map(mut self, f: impl FnOnce(&mut T) -> &mut U) -> ArenaRef { ArenaRef { - ptr: unsafe { NonNull::new_unchecked(f(&mut *self)) }, + ptr: f(&mut self), valid: self.valid, } } @@ -123,7 +121,7 @@ impl Deref for ArenaRef { #[inline(always)] fn deref(&self) -> &Self::Target { self.validate(); - unsafe { self.ptr.as_ref() } + unsafe { &*self.ptr } } } @@ -131,7 +129,7 @@ impl DerefMut for ArenaRef { #[inline(always)] fn deref_mut(&mut self) -> &mut Self::Target { self.validate(); - unsafe { self.ptr.as_mut() } + unsafe { &mut *self.ptr } } } diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index e51c8b2a2eab75ff33eaab746eddbfb250ef12d6..74b414bb03ac98b5bfcd7617521ef5122171155b 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1105,11 +1105,14 @@ impl Interactivity { stacking_order: cx.stacking_order().clone(), }; - if let Some(mouse_cursor) = style.mouse_cursor { - let mouse_position = &cx.mouse_position(); - let hovered = interactive_bounds.visibly_contains(mouse_position, cx); - if hovered { - cx.set_cursor_style(mouse_cursor); + if !cx.has_active_drag() { + if let Some(mouse_cursor) = style.mouse_cursor { + let mouse_position = &cx.mouse_position(); + let hovered = + interactive_bounds.visibly_contains(mouse_position, cx); + if hovered { + cx.set_cursor_style(mouse_cursor); + } } } @@ -1334,8 +1337,8 @@ impl Interactivity { } let is_hovered = interactive_bounds .visibly_contains(&event.position, cx) - && !cx.has_active_drag() - && has_mouse_down.borrow().is_none(); + && has_mouse_down.borrow().is_none() + && !cx.has_active_drag(); let mut was_hovered = was_hovered.borrow_mut(); if is_hovered != was_hovered.clone() { @@ -1420,14 +1423,14 @@ impl Interactivity { } } - let clicked_state = element_state + let active_state = element_state .clicked_state .get_or_insert_with(Default::default) .clone(); - if clicked_state.borrow().is_clicked() { + if active_state.borrow().is_clicked() { cx.on_mouse_event(move |_: &MouseUpEvent, phase, cx| { if phase == DispatchPhase::Capture { - *clicked_state.borrow_mut() = ElementClickedState::default(); + *active_state.borrow_mut() = ElementClickedState::default(); cx.notify(); } }); @@ -1444,7 +1447,7 @@ impl Interactivity { let element = interactive_bounds.visibly_contains(&down.position, cx); if group || element { - *clicked_state.borrow_mut() = + *active_state.borrow_mut() = ElementClickedState { group, element }; cx.notify(); } @@ -1465,7 +1468,6 @@ impl Interactivity { let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); let interactive_bounds = interactive_bounds.clone(); - cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, cx) @@ -1524,7 +1526,15 @@ impl Interactivity { cx.on_action(action_type, listener) } - f(&style, scroll_offset.unwrap_or_default(), cx) + cx.with_z_index(style.z_index.unwrap_or(0), |cx| { + if style.background.as_ref().is_some_and(|fill| { + fill.color().is_some_and(|color| !color.is_transparent()) + }) { + cx.add_opaque_layer(bounds) + } + + f(&style, scroll_offset.unwrap_or_default(), cx) + }) }, ); diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 53d8a3cb95f9d72592c7ea33ac5a87476499b51d..c26cfe612141d9dc3442308012d780af7ab153c5 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -91,6 +91,14 @@ impl UniformListScrollHandle { } } } + + pub fn scroll_top(&self) -> Pixels { + if let Some(state) = &*self.0.borrow() { + -state.scroll_offset.borrow().y + } else { + Pixels::ZERO + } + } } impl Styled for UniformList { @@ -135,6 +143,7 @@ impl Element for UniformList { item_size.width } }); + let height = match available_space.height { AvailableSpace::Definite(height) => desired_height.min(height), AvailableSpace::MinContent | AvailableSpace::MaxContent => { diff --git a/crates/gpui2/src/key_dispatch.rs b/crates/gpui2/src/key_dispatch.rs index a9d717ea1acb6866ab75165bcc4c6daedd268899..03adf887b5aa9e4a0b1cb77cefc81c5adb2e0529 100644 --- a/crates/gpui2/src/key_dispatch.rs +++ b/crates/gpui2/src/key_dispatch.rs @@ -35,7 +35,6 @@ pub(crate) struct DispatchNode { type KeyListener = ArenaRef; -#[derive(Clone)] pub(crate) struct DispatchActionListener { pub(crate) action_type: TypeId, pub(crate) listener: ArenaRef, @@ -267,6 +266,10 @@ impl DispatchTree { &self.nodes[node_id.0] } + pub fn node_mut(&mut self, node_id: DispatchNodeId) -> &mut DispatchNode { + &mut self.nodes[node_id.0] + } + fn active_node(&mut self) -> &mut DispatchNode { let active_node_id = self.active_node_id(); &mut self.nodes[active_node_id.0] diff --git a/crates/gpui2/src/scene.rs b/crates/gpui2/src/scene.rs index 811b2b6e30897f4fe23913e879608c02a26b98bd..e6b601b62f17cf087748f8ce4e672eab14b13fb8 100644 --- a/crates/gpui2/src/scene.rs +++ b/crates/gpui2/src/scene.rs @@ -104,6 +104,15 @@ impl SceneBuilder { ); } + for (ix, surface) in self.surfaces.iter().enumerate() { + let z = layer_z_values[surface.order as LayerId as usize]; + self.splitter.add( + surface + .bounds + .to_bsp_polygon(z, (PrimitiveKind::Surface, ix)), + ); + } + // Sort all polygons, then reassign the order field of each primitive to `draw_order` // We need primitives to be repr(C), hence the weird reuse of the order field for two different types. for (draw_order, polygon) in self diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 07be281f0aafede051b21ef9f4486fd3d8525246..8546e094d41f63f2ecd6725c178ba495ae74017d 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1572,30 +1572,43 @@ impl<'a> WindowContext<'a> { self.propagate_event = true; for node_id in &dispatch_path { - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); - + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); if let Some(context) = node.context.clone() { context_stack.push(context); } - for key_listener in node.key_listeners.clone() { + let key_listeners = mem::take(&mut node.key_listeners); + for key_listener in &key_listeners { key_listener(event, DispatchPhase::Capture, self); if !self.propagate_event { - return; + break; } } + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + node.key_listeners = key_listeners; + + if !self.propagate_event { + return; + } } // Bubble phase for node_id in dispatch_path.iter().rev() { // Handle low level key events - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); - for key_listener in node.key_listeners.clone() { + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + let key_listeners = mem::take(&mut node.key_listeners); + for key_listener in &key_listeners { key_listener(event, DispatchPhase::Bubble, self); if !self.propagate_event { - return; + break; } } + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + node.key_listeners = key_listeners; + + if !self.propagate_event { + return; + } // Match keystrokes let node = self.window.rendered_frame.dispatch_tree.node(*node_id); @@ -1639,38 +1652,52 @@ impl<'a> WindowContext<'a> { // Capture phase for node_id in &dispatch_path { - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + let action_listeners = mem::take(&mut node.action_listeners); for DispatchActionListener { action_type, listener, - } in node.action_listeners.clone() + } in &action_listeners { let any_action = action.as_any(); - if action_type == any_action.type_id() { + if *action_type == any_action.type_id() { listener(any_action, DispatchPhase::Capture, self); if !self.propagate_event { - return; + break; } } } + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + node.action_listeners = action_listeners; + + if !self.propagate_event { + return; + } } // Bubble phase for node_id in dispatch_path.iter().rev() { - let node = self.window.rendered_frame.dispatch_tree.node(*node_id); + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + let action_listeners = mem::take(&mut node.action_listeners); for DispatchActionListener { action_type, listener, - } in node.action_listeners.clone() + } in &action_listeners { let any_action = action.as_any(); - if action_type == any_action.type_id() { + if *action_type == any_action.type_id() { self.propagate_event = false; // Actions stop propagation by default during the bubble phase listener(any_action, DispatchPhase::Bubble, self); if !self.propagate_event { - return; + break; } } } + + let node = self.window.rendered_frame.dispatch_tree.node_mut(*node_id); + node.action_listeners = action_listeners; + if !self.propagate_event { + return; + } } } @@ -2057,9 +2084,14 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { size: self.window().viewport_size, }, }; + let new_stacking_order_id = + post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); + let old_stacking_order = mem::take(&mut self.window_mut().next_frame.z_index_stack); + self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; self.window_mut().next_frame.content_mask_stack.push(mask); let result = f(self); self.window_mut().next_frame.content_mask_stack.pop(); + self.window_mut().next_frame.z_index_stack = old_stacking_order; result } @@ -2068,9 +2100,14 @@ pub trait BorrowWindow: BorrowMut + BorrowMut { fn with_z_index(&mut self, z_index: u8, f: impl FnOnce(&mut Self) -> R) -> R { let new_stacking_order_id = post_inc(&mut self.window_mut().next_frame.next_stacking_order_id); + let old_stacking_order_id = mem::replace( + &mut self.window_mut().next_frame.z_index_stack.id, + new_stacking_order_id, + ); self.window_mut().next_frame.z_index_stack.id = new_stacking_order_id; self.window_mut().next_frame.z_index_stack.push(z_index); let result = f(self); + self.window_mut().next_frame.z_index_stack.id = old_stacking_order_id; self.window_mut().next_frame.z_index_stack.pop(); result } diff --git a/crates/language_tools2/Cargo.toml b/crates/language_tools2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..bf5202cda9959556a5d225c8b1ce01591d7c8eac --- /dev/null +++ b/crates/language_tools2/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "language_tools2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/language_tools.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +language = { package = "language2", path = "../language2" } +project = { package = "project2", path = "../project2" } +workspace = { package = "workspace2", path = "../workspace2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +util = { path = "../util" } +lsp = { package = "lsp2", path = "../lsp2" } +futures.workspace = true +serde.workspace = true +anyhow.workspace = true +tree-sitter.workspace = true + +[dev-dependencies] +client = { package = "client2", path = "../client2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +util = { path = "../util", features = ["test-support"] } +env_logger.workspace = true +unindent.workspace = true diff --git a/crates/language_tools2/src/language_tools.rs b/crates/language_tools2/src/language_tools.rs new file mode 100644 index 0000000000000000000000000000000000000000..0a1f31f03fe82eece9911a9ecc474cd714a364c4 --- /dev/null +++ b/crates/language_tools2/src/language_tools.rs @@ -0,0 +1,15 @@ +mod lsp_log; +mod syntax_tree_view; + +#[cfg(test)] +mod lsp_log_tests; + +use gpui::AppContext; + +pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView}; +pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView}; + +pub fn init(cx: &mut AppContext) { + lsp_log::init(cx); + syntax_tree_view::init(cx); +} diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs new file mode 100644 index 0000000000000000000000000000000000000000..597c9c5b543fb5976c8019981216396b83739d9d --- /dev/null +++ b/crates/language_tools2/src/lsp_log.rs @@ -0,0 +1,896 @@ +use collections::{HashMap, VecDeque}; +use editor::{Editor, EditorElement, EditorEvent, MoveToEnd}; +use futures::{channel::mpsc, StreamExt}; +use gpui::{ + actions, div, AnchorCorner, AnyElement, AppContext, Context, Div, EventEmitter, FocusHandle, + FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, + View, ViewContext, VisualContext, WeakModel, WindowContext, +}; +use language::{LanguageServerId, LanguageServerName}; +use lsp::IoKind; +use project::{search::SearchQuery, Project}; +use std::{borrow::Cow, sync::Arc}; +use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection}; +use workspace::{ + item::{Item, ItemHandle}, + searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +const SEND_LINE: &str = "// Send:"; +const RECEIVE_LINE: &str = "// Receive:"; +const MAX_STORED_LOG_ENTRIES: usize = 2000; + +pub struct LogStore { + projects: HashMap, ProjectState>, + io_tx: mpsc::UnboundedSender<(WeakModel, LanguageServerId, IoKind, String)>, +} + +struct ProjectState { + servers: HashMap, + _subscriptions: [gpui::Subscription; 2], +} + +struct LanguageServerState { + log_messages: VecDeque, + rpc_state: Option, + _io_logs_subscription: Option, + _lsp_logs_subscription: Option, +} + +struct LanguageServerRpcState { + rpc_messages: VecDeque, + last_message_kind: Option, +} + +pub struct LspLogView { + pub(crate) editor: View, + editor_subscription: Subscription, + log_store: Model, + current_server_id: Option, + is_showing_rpc_trace: bool, + project: Model, + focus_handle: FocusHandle, + _log_store_subscriptions: Vec, +} + +pub struct LspLogToolbarItemView { + log_view: Option>, + _log_view_subscription: Option, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum MessageKind { + Send, + Receive, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct LogMenuItem { + pub server_id: LanguageServerId, + pub server_name: LanguageServerName, + pub worktree_root_name: String, + pub rpc_trace_enabled: bool, + pub rpc_trace_selected: bool, + pub logs_selected: bool, +} + +actions!(debug, [OpenLanguageServerLogs]); + +pub fn init(cx: &mut AppContext) { + let log_store = cx.build_model(|cx| LogStore::new(cx)); + + cx.observe_new_views(move |workspace: &mut Workspace, cx| { + let project = workspace.project(); + if project.read(cx).is_local() { + log_store.update(cx, |store, cx| { + store.add_project(&project, cx); + }); + } + + let log_store = log_store.clone(); + workspace.register_action(move |workspace, _: &OpenLanguageServerLogs, cx| { + let project = workspace.project().read(cx); + if project.is_local() { + workspace.add_item( + Box::new(cx.build_view(|cx| { + LspLogView::new(workspace.project().clone(), log_store.clone(), cx) + })), + cx, + ); + } + }); + }) + .detach(); +} + +impl LogStore { + pub fn new(cx: &mut ModelContext) -> Self { + let (io_tx, mut io_rx) = mpsc::unbounded(); + let this = Self { + projects: HashMap::default(), + io_tx, + }; + cx.spawn(|this, mut cx| async move { + while let Some((project, server_id, io_kind, message)) = io_rx.next().await { + if let Some(this) = this.upgrade() { + this.update(&mut cx, |this, cx| { + this.on_io(project, server_id, io_kind, &message, cx); + })?; + } + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + this + } + + pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { + let weak_project = project.downgrade(); + self.projects.insert( + project.downgrade(), + ProjectState { + servers: HashMap::default(), + _subscriptions: [ + cx.observe_release(project, move |this, _, _| { + this.projects.remove(&weak_project); + }), + cx.subscribe(project, |this, project, event, cx| match event { + project::Event::LanguageServerAdded(id) => { + this.add_language_server(&project, *id, cx); + } + project::Event::LanguageServerRemoved(id) => { + this.remove_language_server(&project, *id, cx); + } + project::Event::LanguageServerLog(id, message) => { + this.add_language_server_log(&project, *id, message, cx); + } + _ => {} + }), + ], + }, + ); + } + + fn add_language_server( + &mut self, + project: &Model, + id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option<&mut LanguageServerState> { + let project_state = self.projects.get_mut(&project.downgrade())?; + let server_state = project_state.servers.entry(id).or_insert_with(|| { + cx.notify(); + LanguageServerState { + rpc_state: None, + log_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + _io_logs_subscription: None, + _lsp_logs_subscription: None, + } + }); + + let server = project.read(cx).language_server_for_id(id); + if let Some(server) = server.as_deref() { + if server.has_notification_handler::() { + // Another event wants to re-add the server that was already added and subscribed to, avoid doing it again. + return Some(server_state); + } + } + + let weak_project = project.downgrade(); + let io_tx = self.io_tx.clone(); + server_state._io_logs_subscription = server.as_ref().map(|server| { + server.on_io(move |io_kind, message| { + io_tx + .unbounded_send((weak_project.clone(), id, io_kind, message.to_string())) + .ok(); + }) + }); + let this = cx.handle().downgrade(); + let weak_project = project.downgrade(); + server_state._lsp_logs_subscription = server.map(|server| { + let server_id = server.server_id(); + server.on_notification::({ + move |params, mut cx| { + if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade()) { + this.update(&mut cx, |this, cx| { + this.add_language_server_log(&project, server_id, ¶ms.message, cx); + }) + .ok(); + } + } + }) + }); + Some(server_state) + } + + fn add_language_server_log( + &mut self, + project: &Model, + id: LanguageServerId, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + let language_server_state = match self + .projects + .get_mut(&project.downgrade())? + .servers + .get_mut(&id) + { + Some(existing_state) => existing_state, + None => self.add_language_server(&project, id, cx)?, + }; + + let log_lines = &mut language_server_state.log_messages; + while log_lines.len() >= MAX_STORED_LOG_ENTRIES { + log_lines.pop_front(); + } + let message = message.trim(); + log_lines.push_back(message.to_string()); + cx.emit(Event::NewServerLogEntry { + id, + entry: message.to_string(), + is_rpc: false, + }); + cx.notify(); + Some(()) + } + + fn remove_language_server( + &mut self, + project: &Model, + id: LanguageServerId, + cx: &mut ModelContext, + ) -> Option<()> { + let project_state = self.projects.get_mut(&project.downgrade())?; + project_state.servers.remove(&id); + cx.notify(); + Some(()) + } + + fn server_logs( + &self, + project: &Model, + server_id: LanguageServerId, + ) -> Option<&VecDeque> { + let weak_project = project.downgrade(); + let project_state = self.projects.get(&weak_project)?; + let server_state = project_state.servers.get(&server_id)?; + Some(&server_state.log_messages) + } + + fn enable_rpc_trace_for_language_server( + &mut self, + project: &Model, + server_id: LanguageServerId, + ) -> Option<&mut LanguageServerRpcState> { + let weak_project = project.downgrade(); + let project_state = self.projects.get_mut(&weak_project)?; + let server_state = project_state.servers.get_mut(&server_id)?; + let rpc_state = server_state + .rpc_state + .get_or_insert_with(|| LanguageServerRpcState { + rpc_messages: VecDeque::with_capacity(MAX_STORED_LOG_ENTRIES), + last_message_kind: None, + }); + Some(rpc_state) + } + + pub fn disable_rpc_trace_for_language_server( + &mut self, + project: &Model, + server_id: LanguageServerId, + _: &mut ModelContext, + ) -> Option<()> { + let project = project.downgrade(); + let project_state = self.projects.get_mut(&project)?; + let server_state = project_state.servers.get_mut(&server_id)?; + server_state.rpc_state.take(); + Some(()) + } + + fn on_io( + &mut self, + project: WeakModel, + language_server_id: LanguageServerId, + io_kind: IoKind, + message: &str, + cx: &mut ModelContext, + ) -> Option<()> { + let is_received = match io_kind { + IoKind::StdOut => true, + IoKind::StdIn => false, + IoKind::StdErr => { + let project = project.upgrade()?; + let message = format!("stderr: {}", message.trim()); + self.add_language_server_log(&project, language_server_id, &message, cx); + return Some(()); + } + }; + + let state = self + .projects + .get_mut(&project)? + .servers + .get_mut(&language_server_id)? + .rpc_state + .as_mut()?; + let kind = if is_received { + MessageKind::Receive + } else { + MessageKind::Send + }; + + let rpc_log_lines = &mut state.rpc_messages; + if state.last_message_kind != Some(kind) { + let line_before_message = match kind { + MessageKind::Send => SEND_LINE, + MessageKind::Receive => RECEIVE_LINE, + }; + rpc_log_lines.push_back(line_before_message.to_string()); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + entry: line_before_message.to_string(), + is_rpc: true, + }); + } + + while rpc_log_lines.len() >= MAX_STORED_LOG_ENTRIES { + rpc_log_lines.pop_front(); + } + let message = message.trim(); + rpc_log_lines.push_back(message.to_string()); + cx.emit(Event::NewServerLogEntry { + id: language_server_id, + entry: message.to_string(), + is_rpc: true, + }); + cx.notify(); + Some(()) + } +} + +impl LspLogView { + pub fn new( + project: Model, + log_store: Model, + cx: &mut ViewContext, + ) -> Self { + let server_id = log_store + .read(cx) + .projects + .get(&project.downgrade()) + .and_then(|project| project.servers.keys().copied().next()); + let model_changes_subscription = cx.observe(&log_store, |this, store, cx| { + (|| -> Option<()> { + let project_state = store.read(cx).projects.get(&this.project.downgrade())?; + if let Some(current_lsp) = this.current_server_id { + if !project_state.servers.contains_key(¤t_lsp) { + if let Some(server) = project_state.servers.iter().next() { + if this.is_showing_rpc_trace { + this.show_rpc_trace_for_server(*server.0, cx) + } else { + this.show_logs_for_server(*server.0, cx) + } + } else { + this.current_server_id = None; + this.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + cx.notify(); + } + } + } else { + if let Some(server) = project_state.servers.iter().next() { + if this.is_showing_rpc_trace { + this.show_rpc_trace_for_server(*server.0, cx) + } else { + this.show_logs_for_server(*server.0, cx) + } + } + } + + Some(()) + })(); + + cx.notify(); + }); + let events_subscriptions = cx.subscribe(&log_store, |log_view, _, e, cx| match e { + Event::NewServerLogEntry { id, entry, is_rpc } => { + if log_view.current_server_id == Some(*id) { + if (*is_rpc && log_view.is_showing_rpc_trace) + || (!*is_rpc && !log_view.is_showing_rpc_trace) + { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.handle_input(entry.trim(), cx); + editor.handle_input("\n", cx); + editor.set_read_only(true); + }); + } + } + } + }); + let (editor, editor_subscription) = Self::editor_for_logs(String::new(), cx); + + let focus_handle = cx.focus_handle(); + let focus_subscription = cx.on_focus(&focus_handle, |log_view, cx| { + cx.focus_view(&log_view.editor); + }); + + let mut this = Self { + focus_handle, + editor, + editor_subscription, + project, + log_store, + current_server_id: None, + is_showing_rpc_trace: false, + _log_store_subscriptions: vec![ + model_changes_subscription, + events_subscriptions, + focus_subscription, + ], + }; + if let Some(server_id) = server_id { + this.show_logs_for_server(server_id, cx); + } + this + } + + fn editor_for_logs( + log_contents: String, + cx: &mut ViewContext, + ) -> (View, Subscription) { + let editor = cx.build_view(|cx| { + let mut editor = Editor::multi_line(cx); + editor.set_text(log_contents, cx); + editor.move_to_end(&MoveToEnd, cx); + editor.set_read_only(true); + editor + }); + let editor_subscription = cx.subscribe( + &editor, + |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| { + cx.emit(event.clone()) + }, + ); + (editor, editor_subscription) + } + + pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { + let log_store = self.log_store.read(cx); + let state = log_store.projects.get(&self.project.downgrade())?; + let mut rows = self + .project + .read(cx) + .language_servers() + .filter_map(|(server_id, language_server_name, worktree_id)| { + let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; + let state = state.servers.get(&server_id)?; + Some(LogMenuItem { + server_id, + server_name: language_server_name, + worktree_root_name: worktree.read(cx).root_name().to_string(), + rpc_trace_enabled: state.rpc_state.is_some(), + rpc_trace_selected: self.is_showing_rpc_trace + && self.current_server_id == Some(server_id), + logs_selected: !self.is_showing_rpc_trace + && self.current_server_id == Some(server_id), + }) + }) + .chain( + self.project + .read(cx) + .supplementary_language_servers() + .filter_map(|(&server_id, (name, _))| { + let state = state.servers.get(&server_id)?; + Some(LogMenuItem { + server_id, + server_name: name.clone(), + worktree_root_name: "supplementary".to_string(), + rpc_trace_enabled: state.rpc_state.is_some(), + rpc_trace_selected: self.is_showing_rpc_trace + && self.current_server_id == Some(server_id), + logs_selected: !self.is_showing_rpc_trace + && self.current_server_id == Some(server_id), + }) + }), + ) + .collect::>(); + rows.sort_by_key(|row| row.server_id); + rows.dedup_by_key(|row| row.server_id); + Some(rows) + } + + fn show_logs_for_server(&mut self, server_id: LanguageServerId, cx: &mut ViewContext) { + let log_contents = self + .log_store + .read(cx) + .server_logs(&self.project, server_id) + .map(log_contents); + if let Some(log_contents) = log_contents { + self.current_server_id = Some(server_id); + self.is_showing_rpc_trace = false; + let (editor, editor_subscription) = Self::editor_for_logs(log_contents, cx); + self.editor = editor; + self.editor_subscription = editor_subscription; + cx.notify(); + } + cx.focus(&self.focus_handle); + } + + fn show_rpc_trace_for_server( + &mut self, + server_id: LanguageServerId, + cx: &mut ViewContext, + ) { + let rpc_log = self.log_store.update(cx, |log_store, _| { + log_store + .enable_rpc_trace_for_language_server(&self.project, server_id) + .map(|state| log_contents(&state.rpc_messages)) + }); + if let Some(rpc_log) = rpc_log { + self.current_server_id = Some(server_id); + self.is_showing_rpc_trace = true; + let (editor, editor_subscription) = Self::editor_for_logs(rpc_log, cx); + let language = self.project.read(cx).languages().language_for_name("JSON"); + editor + .read(cx) + .buffer() + .read(cx) + .as_singleton() + .expect("log buffer should be a singleton") + .update(cx, |_, cx| { + cx.spawn({ + let buffer = cx.handle(); + |_, mut cx| async move { + let language = language.await.ok(); + buffer.update(&mut cx, |buffer, cx| { + buffer.set_language(language, cx); + }) + } + }) + .detach_and_log_err(cx); + }); + + self.editor = editor; + self.editor_subscription = editor_subscription; + cx.notify(); + } + + cx.focus(&self.focus_handle); + } + + fn toggle_rpc_trace_for_server( + &mut self, + server_id: LanguageServerId, + enabled: bool, + cx: &mut ViewContext, + ) { + self.log_store.update(cx, |log_store, cx| { + if enabled { + log_store.enable_rpc_trace_for_language_server(&self.project, server_id); + } else { + log_store.disable_rpc_trace_for_language_server(&self.project, server_id, cx); + } + }); + if !enabled && Some(server_id) == self.current_server_id { + self.show_logs_for_server(server_id, cx); + cx.notify(); + } + } +} + +fn log_contents(lines: &VecDeque) -> String { + let (a, b) = lines.as_slices(); + let log_contents = a.join("\n"); + if b.is_empty() { + log_contents + } else { + log_contents + "\n" + &b.join("\n") + } +} + +impl Render for LspLogView { + type Element = EditorElement; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + self.editor.update(cx, |editor, cx| editor.render(cx)) + } +} + +impl FocusableView for LspLogView { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for LspLogView { + type Event = EditorEvent; + + fn to_item_events(event: &Self::Event, f: impl FnMut(workspace::item::ItemEvent)) { + Editor::to_item_events(event, f) + } + + fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { + Label::new("LSP Logs").into_any_element() + } + + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } +} + +impl SearchableItem for LspLogView { + type Match = ::Match; + + fn clear_matches(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |e, cx| e.clear_matches(cx)) + } + + fn update_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.update_matches(matches, cx)) + } + + fn query_suggestion(&mut self, cx: &mut ViewContext) -> String { + self.editor.update(cx, |e, cx| e.query_suggestion(cx)) + } + + fn activate_match( + &mut self, + index: usize, + matches: Vec, + cx: &mut ViewContext, + ) { + self.editor + .update(cx, |e, cx| e.activate_match(index, matches, cx)) + } + + fn select_matches(&mut self, matches: Vec, cx: &mut ViewContext) { + self.editor + .update(cx, |e, cx| e.select_matches(matches, cx)) + } + + fn find_matches( + &mut self, + query: Arc, + cx: &mut ViewContext, + ) -> gpui::Task> { + self.editor.update(cx, |e, cx| e.find_matches(query, cx)) + } + + fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext) { + // Since LSP Log is read-only, it doesn't make sense to support replace operation. + } + fn supported_options() -> workspace::searchable::SearchOptions { + workspace::searchable::SearchOptions { + case: true, + word: true, + regex: true, + // LSP log is read-only. + replacement: false, + } + } + fn active_match_index( + &mut self, + matches: Vec, + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |e, cx| e.active_match_index(matches, cx)) + } +} + +impl EventEmitter for LspLogToolbarItemView {} + +impl ToolbarItemView for LspLogToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + if let Some(item) = active_pane_item { + if let Some(log_view) = item.downcast::() { + self.log_view = Some(log_view.clone()); + self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { + cx.notify(); + })); + return ToolbarItemLocation::PrimaryLeft; + } + } + self.log_view = None; + self._log_view_subscription = None; + ToolbarItemLocation::Hidden + } +} + +impl Render for LspLogToolbarItemView { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let Some(log_view) = self.log_view.clone() else { + return div(); + }; + let (menu_rows, current_server_id) = log_view.update(cx, |log_view, cx| { + let menu_rows = log_view.menu_items(cx).unwrap_or_default(); + let current_server_id = log_view.current_server_id; + (menu_rows, current_server_id) + }); + + let current_server = current_server_id.and_then(|current_server_id| { + if let Ok(ix) = menu_rows.binary_search_by_key(¤t_server_id, |e| e.server_id) { + Some(menu_rows[ix].clone()) + } else { + None + } + }); + + let log_toolbar_view = cx.view().clone(); + let lsp_menu = popover_menu("LspLogView") + .anchor(AnchorCorner::TopLeft) + .trigger(Button::new( + "language_server_menu_header", + current_server + .and_then(|row| { + Some(Cow::Owned(format!( + "{} ({}) - {}", + row.server_name.0, + row.worktree_root_name, + if row.rpc_trace_selected { + RPC_MESSAGES + } else { + SERVER_LOGS + }, + ))) + }) + .unwrap_or_else(|| "No server selected".into()), + )) + .menu(move |cx| { + let menu_rows = menu_rows.clone(); + let log_view = log_view.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for (ix, row) in menu_rows.into_iter().enumerate() { + let server_selected = Some(row.server_id) == current_server_id; + menu = menu + .header(format!( + "{} ({})", + row.server_name.0, row.worktree_root_name + )) + .entry( + SERVER_LOGS, + cx.handler_for(&log_view, move |view, cx| { + view.show_logs_for_server(row.server_id, cx); + }), + ); + if server_selected && row.logs_selected { + debug_assert_eq!( + Some(ix * 3 + 1), + menu.select_last(), + "Could not scroll to a just added LSP menu item" + ); + } + + menu = menu.custom_entry( + { + let log_toolbar_view = log_toolbar_view.clone(); + move |cx| { + h_stack() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .child( + div().z_index(120).child( + Checkbox::new( + ix, + if row.rpc_trace_enabled { + Selection::Selected + } else { + Selection::Unselected + }, + ) + .on_click(cx.listener_for( + &log_toolbar_view, + move |view, selection, cx| { + let enabled = matches!( + selection, + Selection::Selected + ); + view.toggle_logging_for_server( + row.server_id, + enabled, + cx, + ); + cx.stop_propagation(); + }, + )), + ), + ) + .into_any_element() + } + }, + cx.handler_for(&log_view, move |view, cx| { + view.show_rpc_trace_for_server(row.server_id, cx); + }), + ); + if server_selected && row.rpc_trace_selected { + debug_assert_eq!( + Some(ix * 3 + 2), + menu.select_last(), + "Could not scroll to a just added LSP menu item" + ); + } + } + menu + }) + }); + + h_stack().size_full().child(lsp_menu).child( + div() + .child( + Button::new("clear_log_button", "Clear").on_click(cx.listener( + |this, _, cx| { + if let Some(log_view) = this.log_view.as_ref() { + log_view.update(cx, |log_view, cx| { + log_view.editor.update(cx, |editor, cx| { + editor.set_read_only(false); + editor.clear(cx); + editor.set_read_only(true); + }); + }) + } + }, + )), + ) + .ml_2(), + ) + } +} + +const RPC_MESSAGES: &str = "RPC Messages"; +const SERVER_LOGS: &str = "Server Logs"; + +impl LspLogToolbarItemView { + pub fn new() -> Self { + Self { + log_view: None, + _log_view_subscription: None, + } + } + + fn toggle_logging_for_server( + &mut self, + id: LanguageServerId, + enabled: bool, + cx: &mut ViewContext, + ) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |log_view, cx| { + log_view.toggle_rpc_trace_for_server(id, enabled, cx); + if !enabled && Some(id) == log_view.current_server_id { + log_view.show_logs_for_server(id, cx); + cx.notify(); + } + cx.focus(&log_view.focus_handle); + }); + } + cx.notify(); + } +} + +pub enum Event { + NewServerLogEntry { + id: LanguageServerId, + entry: String, + is_rpc: bool, + }, +} + +impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} +impl EventEmitter for LspLogView {} +impl EventEmitter for LspLogView {} diff --git a/crates/language_tools2/src/lsp_log_tests.rs b/crates/language_tools2/src/lsp_log_tests.rs new file mode 100644 index 0000000000000000000000000000000000000000..93e869369afa57bec6c14af6d1929b920dbf7345 --- /dev/null +++ b/crates/language_tools2/src/lsp_log_tests.rs @@ -0,0 +1,107 @@ +use std::sync::Arc; + +use crate::lsp_log::LogMenuItem; + +use super::*; +use futures::StreamExt; +use gpui::{serde_json::json, Context, TestAppContext, VisualTestContext}; +use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; +use project::{FakeFs, Project}; +use settings::SettingsStore; + +#[gpui::test] +async fn test_lsp_logs(cx: &mut TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } + + init_test(cx); + + let mut rust_language = Language::new( + LanguageConfig { + name: "Rust".into(), + path_suffixes: vec!["rs".to_string()], + ..Default::default() + }, + Some(tree_sitter_rust::language()), + ); + let mut fake_rust_servers = rust_language + .set_fake_lsp_adapter(Arc::new(FakeLspAdapter { + name: "the-rust-language-server", + ..Default::default() + })) + .await; + + let fs = FakeFs::new(cx.background_executor.clone()); + fs.insert_tree( + "/the-root", + json!({ + "test.rs": "", + "package.json": "", + }), + ) + .await; + let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await; + project.update(cx, |project, _| { + project.languages().add(Arc::new(rust_language)); + }); + + let log_store = cx.build_model(|cx| LogStore::new(cx)); + log_store.update(cx, |store, cx| store.add_project(&project, cx)); + + let _rust_buffer = project + .update(cx, |project, cx| { + project.open_local_buffer("/the-root/test.rs", cx) + }) + .await + .unwrap(); + + let mut language_server = fake_rust_servers.next().await.unwrap(); + language_server + .receive_notification::() + .await; + + let window = cx.add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)); + let log_view = window.root(cx).unwrap(); + let mut cx = VisualTestContext::from_window(*window, cx); + + language_server.notify::(lsp::LogMessageParams { + message: "hello from the server".into(), + typ: lsp::MessageType::INFO, + }); + cx.executor().run_until_parked(); + + log_view.update(&mut cx, |view, cx| { + assert_eq!( + view.menu_items(cx).unwrap(), + &[LogMenuItem { + server_id: language_server.server.server_id(), + server_name: LanguageServerName("the-rust-language-server".into()), + worktree_root_name: project + .read(cx) + .worktrees() + .next() + .unwrap() + .read(cx) + .root_name() + .to_string(), + rpc_trace_enabled: false, + rpc_trace_selected: false, + logs_selected: true, + }] + ); + assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n"); + }); +} + +fn init_test(cx: &mut gpui::TestAppContext) { + cx.update(|cx| { + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + client::init_settings(cx); + Project::init_settings(cx); + editor::init_settings(cx); + }); +} diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..dcdcb612b781e6d93e92650c111f9ea9e1ac9d1f --- /dev/null +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -0,0 +1,535 @@ +use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; +use gpui::{ + actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, + EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, + MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, +}; +use language::{Buffer, OwnedSyntaxLayerInfo}; +use settings::Settings; +use std::{mem, ops::Range}; +use theme::{ActiveTheme, ThemeSettings}; +use tree_sitter::{Node, TreeCursor}; +use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; +use workspace::{ + item::{Item, ItemHandle}, + SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +actions!(debug, [OpenSyntaxTreeView]); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &OpenSyntaxTreeView, cx| { + let active_item = workspace.active_item(cx); + let workspace_handle = workspace.weak_handle(); + let syntax_tree_view = + cx.build_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); + workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx) + }); + }) + .detach(); +} + +pub struct SyntaxTreeView { + workspace_handle: WeakView, + editor: Option, + mouse_y: Option, + line_height: Option, + list_scroll_handle: UniformListScrollHandle, + selected_descendant_ix: Option, + hovered_descendant_ix: Option, + focus_handle: FocusHandle, +} + +pub struct SyntaxTreeToolbarItemView { + tree_view: Option>, + subscription: Option, +} + +struct EditorState { + editor: View, + active_buffer: Option, + _subscription: gpui::Subscription, +} + +#[derive(Clone)] +struct BufferState { + buffer: Model, + excerpt_id: ExcerptId, + active_layer: Option, +} + +impl SyntaxTreeView { + pub fn new( + workspace_handle: WeakView, + active_item: Option>, + cx: &mut ViewContext, + ) -> Self { + let mut this = Self { + workspace_handle: workspace_handle.clone(), + list_scroll_handle: UniformListScrollHandle::new(), + editor: None, + mouse_y: None, + line_height: None, + hovered_descendant_ix: None, + selected_descendant_ix: None, + focus_handle: cx.focus_handle(), + }; + + this.workspace_updated(active_item, cx); + cx.observe( + &workspace_handle.upgrade().unwrap(), + |this, workspace, cx| { + this.workspace_updated(workspace.read(cx).active_item(cx), cx); + }, + ) + .detach(); + + this + } + + fn workspace_updated( + &mut self, + active_item: Option>, + cx: &mut ViewContext, + ) { + if let Some(item) = active_item { + if item.item_id() != cx.entity_id() { + if let Some(editor) = item.act_as::(cx) { + self.set_editor(editor, cx); + } + } + } + } + + fn set_editor(&mut self, editor: View, cx: &mut ViewContext) { + if let Some(state) = &self.editor { + if state.editor == editor { + return; + } + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(cx) + }); + } + + let subscription = cx.subscribe(&editor, |this, _, event, cx| { + let did_reparse = match event { + editor::EditorEvent::Reparsed => true, + editor::EditorEvent::SelectionsChanged { .. } => false, + _ => return, + }; + this.editor_updated(did_reparse, cx); + }); + + self.editor = Some(EditorState { + editor, + _subscription: subscription, + active_buffer: None, + }); + self.editor_updated(true, cx); + } + + fn editor_updated(&mut self, did_reparse: bool, cx: &mut ViewContext) -> Option<()> { + // Find which excerpt the cursor is in, and the position within that excerpted buffer. + let editor_state = self.editor.as_mut()?; + let editor = &editor_state.editor.read(cx); + let selection_range = editor.selections.last::(cx).range(); + let multibuffer = editor.buffer().read(cx); + let (buffer, range, excerpt_id) = multibuffer + .range_to_buffer_ranges(selection_range, cx) + .pop()?; + + // If the cursor has moved into a different excerpt, retrieve a new syntax layer + // from that buffer. + let buffer_state = editor_state + .active_buffer + .get_or_insert_with(|| BufferState { + buffer: buffer.clone(), + excerpt_id, + active_layer: None, + }); + let mut prev_layer = None; + if did_reparse { + prev_layer = buffer_state.active_layer.take(); + } + if buffer_state.buffer != buffer || buffer_state.excerpt_id != buffer_state.excerpt_id { + buffer_state.buffer = buffer.clone(); + buffer_state.excerpt_id = excerpt_id; + buffer_state.active_layer = None; + } + + let layer = match &mut buffer_state.active_layer { + Some(layer) => layer, + None => { + let snapshot = buffer.read(cx).snapshot(); + let layer = if let Some(prev_layer) = prev_layer { + let prev_range = prev_layer.node().byte_range(); + snapshot + .syntax_layers() + .filter(|layer| layer.language == &prev_layer.language) + .min_by_key(|layer| { + let range = layer.node().byte_range(); + ((range.start as i64) - (prev_range.start as i64)).abs() + + ((range.end as i64) - (prev_range.end as i64)).abs() + })? + } else { + snapshot.syntax_layers().next()? + }; + buffer_state.active_layer.insert(layer.to_owned()) + } + }; + + // Within the active layer, find the syntax node under the cursor, + // and scroll to it. + let mut cursor = layer.node().walk(); + while cursor.goto_first_child_for_byte(range.start).is_some() { + if !range.is_empty() && cursor.node().end_byte() == range.start { + cursor.goto_next_sibling(); + } + } + + // Ascend to the smallest ancestor that contains the range. + loop { + let node_range = cursor.node().byte_range(); + if node_range.start <= range.start && node_range.end >= range.end { + break; + } + if !cursor.goto_parent() { + break; + } + } + + let descendant_ix = cursor.descendant_index(); + self.selected_descendant_ix = Some(descendant_ix); + self.list_scroll_handle.scroll_to_item(descendant_ix); + + cx.notify(); + Some(()) + } + + fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext) -> Option<()> { + let line_height = self.line_height?; + let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; + + self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, mut range, cx| { + // Put the cursor at the beginning of the node. + mem::swap(&mut range.start, &mut range.end); + + editor.change_selections(Some(Autoscroll::newest()), cx, |selections| { + selections.select_ranges(vec![range]); + }); + }); + Some(()) + } + + fn hover_state_changed(&mut self, cx: &mut ViewContext) { + if let Some((y, line_height)) = self.mouse_y.zip(self.line_height) { + let ix = ((self.list_scroll_handle.scroll_top() + y) / line_height) as usize; + if self.hovered_descendant_ix != Some(ix) { + self.hovered_descendant_ix = Some(ix); + self.update_editor_with_range_for_descendant_ix(ix, cx, |editor, range, cx| { + editor.clear_background_highlights::(cx); + editor.highlight_background::( + vec![range], + |theme| theme.editor_document_highlight_write_background, + cx, + ); + }); + cx.notify(); + } + } + } + + fn update_editor_with_range_for_descendant_ix( + &self, + descendant_ix: usize, + cx: &mut ViewContext, + mut f: impl FnMut(&mut Editor, Range, &mut ViewContext), + ) -> Option<()> { + let editor_state = self.editor.as_ref()?; + let buffer_state = editor_state.active_buffer.as_ref()?; + let layer = buffer_state.active_layer.as_ref()?; + + // Find the node. + let mut cursor = layer.node().walk(); + cursor.goto_descendant(descendant_ix); + let node = cursor.node(); + let range = node.byte_range(); + + // Build a text anchor range. + let buffer = buffer_state.buffer.read(cx); + let range = buffer.anchor_before(range.start)..buffer.anchor_after(range.end); + + // Build a multibuffer anchor range. + let multibuffer = editor_state.editor.read(cx).buffer(); + let multibuffer = multibuffer.read(cx).snapshot(cx); + let excerpt_id = buffer_state.excerpt_id; + let range = multibuffer.anchor_in_excerpt(excerpt_id, range.start) + ..multibuffer.anchor_in_excerpt(excerpt_id, range.end); + + // Update the editor with the anchor range. + editor_state.editor.update(cx, |editor, cx| { + f(editor, range, cx); + }); + Some(()) + } + + fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &AppContext) -> Div { + let colors = cx.theme().colors(); + let mut row = h_stack(); + if let Some(field_name) = cursor.field_name() { + row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]); + } + + let node = cursor.node(); + return row + .child(if node.is_named() { + Label::new(node.kind()).color(Color::Default) + } else { + Label::new(format!("\"{}\"", node.kind())).color(Color::Created) + }) + .child( + div() + .child(Label::new(format_node_range(node)).color(Color::Muted)) + .pl_1(), + ) + .text_bg(if selected { + colors.element_selected + } else { + Hsla::default() + }) + .pl(rems(depth as f32)) + .hover(|style| style.bg(colors.element_hover)); + } +} + +impl Render for SyntaxTreeView { + type Element = Div; + + fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Self::Element { + let settings = ThemeSettings::get_global(cx); + let line_height = cx + .text_style() + .line_height_in_pixels(settings.buffer_font_size(cx)); + if Some(line_height) != self.line_height { + self.line_height = Some(line_height); + self.hover_state_changed(cx); + } + + let mut rendered = div().flex_1(); + + if let Some(layer) = self + .editor + .as_ref() + .and_then(|editor| editor.active_buffer.as_ref()) + .and_then(|buffer| buffer.active_layer.as_ref()) + { + let layer = layer.clone(); + let list = uniform_list( + cx.view().clone(), + "SyntaxTreeView", + layer.node().descendant_count(), + move |this, range, cx| { + let mut items = Vec::new(); + let mut cursor = layer.node().walk(); + let mut descendant_ix = range.start as usize; + cursor.goto_descendant(descendant_ix); + let mut depth = cursor.depth(); + let mut visited_children = false; + while descendant_ix < range.end { + if visited_children { + if cursor.goto_next_sibling() { + visited_children = false; + } else if cursor.goto_parent() { + depth -= 1; + } else { + break; + } + } else { + items.push(Self::render_node( + &cursor, + depth, + Some(descendant_ix) == this.selected_descendant_ix, + cx, + )); + descendant_ix += 1; + if cursor.goto_first_child() { + depth += 1; + } else { + visited_children = true; + } + } + } + items + }, + ) + .size_full() + .track_scroll(self.list_scroll_handle.clone()) + .on_mouse_move(cx.listener(move |tree_view, event: &MouseMoveEvent, cx| { + tree_view.mouse_y = Some(event.position.y); + tree_view.hover_state_changed(cx); + })) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |tree_view, event: &MouseDownEvent, cx| { + tree_view.handle_click(event.position.y, cx); + }), + ) + .text_bg(cx.theme().colors().background); + + rendered = rendered.child( + canvas(move |bounds, cx| { + list.into_any_element().draw( + bounds.origin, + bounds.size.map(AvailableSpace::Definite), + cx, + ) + }) + .size_full(), + ); + } + + rendered + } +} + +impl EventEmitter<()> for SyntaxTreeView {} + +impl FocusableView for SyntaxTreeView { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for SyntaxTreeView { + type Event = (); + + fn to_item_events(_: &Self::Event, _: impl FnMut(workspace::item::ItemEvent)) {} + + fn tab_content(&self, _: Option, _: bool, _: &WindowContext<'_>) -> AnyElement { + Label::new("Syntax Tree").into_any_element() + } + + fn clone_on_split( + &self, + _: workspace::WorkspaceId, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.build_view(|cx| { + let mut clone = Self::new(self.workspace_handle.clone(), None, cx); + if let Some(editor) = &self.editor { + clone.set_editor(editor.editor.clone(), cx) + } + clone + })) + } +} + +impl SyntaxTreeToolbarItemView { + pub fn new() -> Self { + Self { + tree_view: None, + subscription: None, + } + } + + fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option> { + let tree_view = self.tree_view.as_ref()?; + let tree_view = tree_view.read(cx); + + let editor_state = tree_view.editor.as_ref()?; + let buffer_state = editor_state.active_buffer.as_ref()?; + let active_layer = buffer_state.active_layer.clone()?; + let active_buffer = buffer_state.buffer.read(cx).snapshot(); + + let view = cx.view().clone(); + Some( + popover_menu("Syntax Tree") + .trigger(Self::render_header(&active_layer)) + .menu(move |cx| { + ContextMenu::build(cx, |mut menu, cx| { + for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() { + menu = menu.entry( + format!( + "{} {}", + layer.language.name(), + format_node_range(layer.node()) + ), + cx.handler_for(&view, move |view, cx| { + view.select_layer(layer_ix, cx); + }), + ); + } + menu + }) + }), + ) + } + + fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext) -> Option<()> { + let tree_view = self.tree_view.as_ref()?; + tree_view.update(cx, |view, cx| { + let editor_state = view.editor.as_mut()?; + let buffer_state = editor_state.active_buffer.as_mut()?; + let snapshot = buffer_state.buffer.read(cx).snapshot(); + let layer = snapshot.syntax_layers().nth(layer_ix)?; + buffer_state.active_layer = Some(layer.to_owned()); + view.selected_descendant_ix = None; + cx.notify(); + view.focus_handle.focus(cx); + Some(()) + }) + } + + fn render_header(active_layer: &OwnedSyntaxLayerInfo) -> ButtonLike { + ButtonLike::new("syntax tree header") + .child(Label::new(active_layer.language.name())) + .child(Label::new(format_node_range(active_layer.node()))) + } +} + +fn format_node_range(node: Node) -> String { + let start = node.start_position(); + let end = node.end_position(); + format!( + "[{}:{} - {}:{}]", + start.row + 1, + start.column + 1, + end.row + 1, + end.column + 1, + ) +} + +impl Render for SyntaxTreeToolbarItemView { + type Element = PopoverMenu; + + fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> PopoverMenu { + self.render_menu(cx) + .unwrap_or_else(|| popover_menu("Empty Syntax Tree")) + } +} + +impl EventEmitter for SyntaxTreeToolbarItemView {} + +impl ToolbarItemView for SyntaxTreeToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> ToolbarItemLocation { + if let Some(item) = active_pane_item { + if let Some(view) = item.downcast::() { + self.tree_view = Some(view.clone()); + self.subscription = Some(cx.observe(&view, |_, _, cx| cx.notify())); + return ToolbarItemLocation::PrimaryLeft; + } + } + self.tree_view = None; + self.subscription = None; + ToolbarItemLocation::Hidden + } +} diff --git a/crates/recent_projects2/Cargo.toml b/crates/recent_projects2/Cargo.toml index 3d10c147e0ae69ed257d3787618496c904fe823a..46854ab4e8b9787a237d38644cd776732d8202f5 100644 --- a/crates/recent_projects2/Cargo.toml +++ b/crates/recent_projects2/Cargo.toml @@ -9,7 +9,6 @@ path = "src/recent_projects.rs" doctest = false [dependencies] -db = { path = "../db" } editor = { package = "editor2", path = "../editor2" } fuzzy = { package = "fuzzy2", path = "../fuzzy2" } gpui = { package = "gpui2", path = "../gpui2" } diff --git a/crates/terminal_view2/src/terminal_element.rs b/crates/terminal_view2/src/terminal_element.rs index 07910896f31320d108e45490e3aab70c06a76fb1..f30c1cf1bc186f3d9f121739f6a00bdf59fb0c75 100644 --- a/crates/terminal_view2/src/terminal_element.rs +++ b/crates/terminal_view2/src/terminal_element.rs @@ -395,13 +395,13 @@ impl TerminalElement { let theme = cx.theme().clone(); let link_style = HighlightStyle { - color: Some(gpui::blue()), + color: Some(theme.colors().link_text_hover), font_weight: None, font_style: None, background_color: None, underline: Some(UnderlineStyle { thickness: px(1.0), - color: Some(gpui::red()), + color: Some(theme.colors().link_text_hover), wavy: false, }), fade_out: None, diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8fce15d1c69d0fcf97f8ac1757874be4884950b6..97bb3865ef6dbefd95e4274fe77d37df630aee74 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -3,13 +3,13 @@ use crate::{ ListSeparator, ListSubHeader, }; use gpui::{ - px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, - IntoElement, Render, Subscription, View, VisualContext, + px, Action, AnyElement, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, + FocusableView, IntoElement, Render, Subscription, View, VisualContext, }; use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use std::{rc::Rc, time::Duration}; -pub enum ContextMenuItem { +enum ContextMenuItem { Separator, Header(SharedString), Entry { @@ -18,6 +18,10 @@ pub enum ContextMenuItem { handler: Rc, action: Option>, }, + CustomEntry { + entry_render: Box AnyElement>, + handler: Rc, + }, } pub struct ContextMenu { @@ -83,6 +87,18 @@ impl ContextMenu { self } + pub fn custom_entry( + mut self, + entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static, + handler: impl Fn(&mut WindowContext) + 'static, + ) -> Self { + self.items.push(ContextMenuItem::CustomEntry { + entry_render: Box::new(entry_render), + handler: Rc::new(handler), + }); + self + } + pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), @@ -104,11 +120,14 @@ impl ContextMenu { } pub fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext) { - if let Some(ContextMenuItem::Entry { handler, .. }) = - self.selected_index.and_then(|ix| self.items.get(ix)) - { - (handler)(cx) + match self.selected_index.and_then(|ix| self.items.get(ix)) { + Some( + ContextMenuItem::Entry { handler, .. } + | ContextMenuItem::CustomEntry { handler, .. }, + ) => (handler)(cx), + _ => {} } + cx.emit(DismissEvent); } @@ -122,14 +141,20 @@ impl ContextMenu { cx.notify(); } - fn select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + pub fn select_last(&mut self) -> Option { for (ix, item) in self.items.iter().enumerate().rev() { if item.is_selectable() { self.selected_index = Some(ix); - cx.notify(); - break; + return Some(ix); } } + None + } + + fn handle_select_last(&mut self, _: &SelectLast, cx: &mut ViewContext) { + if self.select_last().is_some() { + cx.notify(); + } } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -156,7 +181,7 @@ impl ContextMenu { } } } else { - self.select_last(&Default::default(), cx); + self.handle_select_last(&Default::default(), cx); } } @@ -182,7 +207,7 @@ impl ContextMenu { .await; this.update(&mut cx, |this, cx| { cx.dispatch_action(action); - this.cancel(&Default::default(), cx) + this.cancel(&menu::Cancel, cx) }) }) .detach_and_log_err(cx); @@ -194,7 +219,7 @@ impl ContextMenu { impl ContextMenuItem { fn is_selectable(&self) -> bool { - matches!(self, Self::Entry { .. }) + matches!(self, Self::Entry { .. } | Self::CustomEntry { .. }) } } @@ -206,10 +231,10 @@ impl Render for ContextMenu { v_stack() .min_w(px(200.)) .track_focus(&self.focus_handle) - .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&Default::default(), cx))) + .on_mouse_down_out(cx.listener(|this, _, cx| this.cancel(&menu::Cancel, cx))) .key_context("menu") .on_action(cx.listener(ContextMenu::select_first)) - .on_action(cx.listener(ContextMenu::select_last)) + .on_action(cx.listener(ContextMenu::handle_select_last)) .on_action(cx.listener(ContextMenu::select_next)) .on_action(cx.listener(ContextMenu::select_prev)) .on_action(cx.listener(ContextMenu::confirm)) @@ -230,50 +255,60 @@ impl Render for ContextMenu { el }) .flex_none() - .child( - List::new().children(self.items.iter().enumerate().map( - |(ix, item)| match item { - ContextMenuItem::Separator => ListSeparator.into_any_element(), - ContextMenuItem::Header(header) => { - ListSubHeader::new(header.clone()).into_any_element() - } - ContextMenuItem::Entry { - label, - handler, - icon, - action, - } => { - let handler = handler.clone(); - - let label_element = if let Some(icon) = icon { - h_stack() - .gap_1() - .child(Label::new(label.clone())) - .child(IconElement::new(*icon)) - .into_any_element() - } else { - Label::new(label.clone()).into_any_element() - }; + .child(List::new().children(self.items.iter_mut().enumerate().map( + |(ix, item)| match item { + ContextMenuItem::Separator => ListSeparator.into_any_element(), + ContextMenuItem::Header(header) => { + ListSubHeader::new(header.clone()).into_any_element() + } + ContextMenuItem::Entry { + label, + handler, + icon, + action, + } => { + let handler = handler.clone(); - ListItem::new(label.clone()) - .inset(true) - .selected(Some(ix) == self.selected_index) - .on_click(move |_, cx| handler(cx)) - .child( - h_stack() - .w_full() - .justify_between() - .child(label_element) - .children(action.as_ref().and_then(|action| { - KeyBinding::for_action(&**action, cx) - .map(|binding| div().ml_1().child(binding)) - })), - ) + let label_element = if let Some(icon) = icon { + h_stack() + .gap_1() + .child(Label::new(label.clone())) + .child(IconElement::new(*icon)) .into_any_element() - } - }, - )), - ), + } else { + Label::new(label.clone()).into_any_element() + }; + + ListItem::new(ix) + .inset(true) + .selected(Some(ix) == self.selected_index) + .on_click(move |_, cx| handler(cx)) + .child( + h_stack() + .w_full() + .justify_between() + .child(label_element) + .children(action.as_ref().and_then(|action| { + KeyBinding::for_action(&**action, cx) + .map(|binding| div().ml_1().child(binding)) + })), + ) + .into_any_element() + } + ContextMenuItem::CustomEntry { + entry_render, + handler, + } => { + let handler = handler.clone(); + ListItem::new(ix) + .inset(true) + .selected(Some(ix) == self.selected_index) + .on_click(move |_, cx| handler(cx)) + .child(entry_render(cx)) + .into_any_element() + } + }, + ))), ) } } diff --git a/crates/ui2/src/components/tab_bar.rs b/crates/ui2/src/components/tab_bar.rs index d2e6e9518bdae89bd31506f1c1d8fad4660881fa..7cff2f51bd80f4dc38df6b9e2423d1627024ea33 100644 --- a/crates/ui2/src/components/tab_bar.rs +++ b/crates/ui2/src/components/tab_bar.rs @@ -96,7 +96,6 @@ impl RenderOnce for TabBar { div() .id(self.id) - .z_index(120) // todo!("z-index") .group("tab_bar") .flex() .flex_none() diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 4d0d4653090ad4ba0e993f21d67d1b6cceaa7553..4d20699528e453718b5afdc142cb7e87047c9fb9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -789,6 +789,7 @@ impl Pane { } self.update_toolbar(cx); + self.update_status_bar(cx); if focus_item { self.focus_active_item(cx); @@ -1450,6 +1451,22 @@ impl Pane { }); } + fn update_status_bar(&mut self, cx: &mut ViewContext) { + let workspace = self.workspace.clone(); + let pane = cx.view().clone(); + + cx.window_context().defer(move |cx| { + let Ok(status_bar) = workspace.update(cx, |workspace, _| workspace.status_bar.clone()) + else { + return; + }; + + status_bar.update(cx, move |status_bar, cx| { + status_bar.set_active_pane(&pane, cx); + }); + }); + } + fn render_tab( &self, ix: usize, diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index ba571d6e0ad3c70bc64139e90e64f9c59314bfc7..f4e4ac9508dc4fae74913c5eefb59c39802b38e7 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -83,6 +83,9 @@ impl StatusBar { where T: 'static + StatusItemView, { + let active_pane_item = self.active_pane.read(cx).active_item(); + item.set_active_pane_item(active_pane_item.as_deref(), cx); + self.left_items.push(Box::new(item)); cx.notify(); } @@ -119,6 +122,9 @@ impl StatusBar { ) where T: 'static + StatusItemView, { + let active_pane_item = self.active_pane.read(cx).active_item(); + item.set_active_pane_item(active_pane_item.as_deref(), cx); + if position < self.left_items.len() { self.left_items.insert(position + 1, Box::new(item)) } else { @@ -141,6 +147,9 @@ impl StatusBar { where T: 'static + StatusItemView, { + let active_pane_item = self.active_pane.read(cx).active_item(); + item.set_active_pane_item(active_pane_item.as_deref(), cx); + self.right_items.push(Box::new(item)); cx.notify(); } diff --git a/crates/workspace2/src/toolbar.rs b/crates/workspace2/src/toolbar.rs index 7436232b04e556ebe8ed648363908471512d1585..cd25582f36cbd397688a500e85c0e405865dae63 100644 --- a/crates/workspace2/src/toolbar.rs +++ b/crates/workspace2/src/toolbar.rs @@ -105,7 +105,6 @@ impl Render for Toolbar { v_stack() .p_1() .gap_2() - .z_index(80) // todo!("z-index") .border_b() .border_color(cx.theme().colors().border_variant) .bg(cx.theme().colors().toolbar_background) diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 0c115fb2850a07afc1d26b348c80d9816418c232..987208880be6beec91dc53ce2679c04aaa386f44 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -3,7 +3,7 @@ authors = ["Nathan Sobo "] description = "The fast, collaborative code editor." edition = "2021" name = "zed" -version = "0.118.0" +version = "0.119.0" publish = false [lib] diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 6646eb5ffc3b47141f1bac1df6a7c4f6658f7f8c..8cc333348462e327d8bf9eded5ef3d72de46f382 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -47,7 +47,7 @@ language = { package = "language2", path = "../language2" } language_selector = { package = "language_selector2", path = "../language_selector2" } lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } -# language_tools = { path = "../language_tools" } +language_tools = { package = "language_tools2", path = "../language_tools2" } node_runtime = { path = "../node_runtime" } notifications = { package = "notifications2", path = "../notifications2" } assistant = { package = "assistant2", path = "../assistant2" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index ca8cd7a2a2a1d608c5c0821aa748de5539c47cd7..f4d9aa2510f4739659f4749fd877d9ea8de76c98 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -217,8 +217,7 @@ fn main() { // journal2::init(app_state.clone(), cx); language_selector::init(cx); theme_selector::init(cx); - // activity_indicator::init(cx); - // language_tools::init(cx); + language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); notifications::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 27e32fff179819fb79f87af303a4f996bc295b26..aa21a3e9955defc8cb623b163851ecd8156dc5c5 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -429,12 +429,11 @@ fn initialize_pane(workspace: &mut Workspace, pane: &View, cx: &mut ViewCo toolbar.add_item(diagnostic_editor_controls, cx); let project_search_bar = cx.build_view(|_| ProjectSearchBar::new()); toolbar.add_item(project_search_bar, cx); - // let lsp_log_item = - // cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); - // toolbar.add_item(lsp_log_item, cx); - // let syntax_tree_item = cx - // .add_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); - // toolbar.add_item(syntax_tree_item, cx); + let lsp_log_item = cx.build_view(|_| language_tools::LspLogToolbarItemView::new()); + toolbar.add_item(lsp_log_item, cx); + let syntax_tree_item = + cx.build_view(|_| language_tools::SyntaxTreeToolbarItemView::new()); + toolbar.add_item(syntax_tree_item, cx); }) }); }