diff --git a/Cargo.lock b/Cargo.lock index cb0d54022f2d4233285fd66735a21625064cb38f..3226037d51a40f06b24ed0ede79e2ee211c2ed43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" @@ -12174,6 +12198,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/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/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/elements/div.rs b/crates/gpui2/src/elements/div.rs index 908b302dfc1cd28f6ff873eff215d583d1b489f4..55791a56a9306a721551ad9755a39c5eb959b293 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1427,7 +1427,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) @@ -1492,6 +1491,7 @@ impl Interactivity { }) { 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 7fba7ef47777553ae6e5e36ab8e20f5e01869360..82eac590977f7c01e53e33a5ca257a9c1a790030 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/window.rs b/crates/gpui2/src/window.rs index 07be281f0aafede051b21ef9f4486fd3d8525246..986458ebf73d4492e0d5c457ab60b95bb5628dcc 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2057,9 +2057,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 +2073,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/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/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/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); }) }); }