From 64925231b0b3a6a40da37fd8d9b124bc3a6abaad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 14 Dec 2023 15:20:49 +0200 Subject: [PATCH 01/14] Create a new crate --- Cargo.lock | 23 + Cargo.toml | 1 + crates/language_tools2/Cargo.toml | 33 + crates/language_tools2/src/language_tools.rs | 15 + crates/language_tools2/src/lsp_log.rs | 1023 +++++++++++++++++ crates/language_tools2/src/lsp_log_tests.rs | 109 ++ .../language_tools2/src/syntax_tree_view.rs | 677 +++++++++++ 7 files changed, 1881 insertions(+) create mode 100644 crates/language_tools2/Cargo.toml create mode 100644 crates/language_tools2/src/language_tools.rs create mode 100644 crates/language_tools2/src/lsp_log.rs create mode 100644 crates/language_tools2/src/lsp_log_tests.rs create mode 100644 crates/language_tools2/src/syntax_tree_view.rs diff --git a/Cargo.lock b/Cargo.lock index cb0d54022f2d4233285fd66735a21625064cb38f..4f418a1813837cdb60f325305aa223a4b1a8a41e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4990,6 +4990,29 @@ 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", + "unindent", + "util", + "workspace2", +] + [[package]] name = "lazy_static" version = "1.4.0" 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/language_tools2/Cargo.toml b/crates/language_tools2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ca3ede2ef9a1e80e32afedec15bf8c0bdf02afbc --- /dev/null +++ b/crates/language_tools2/Cargo.toml @@ -0,0 +1,33 @@ +[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" } +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..9f27c62a04a001a15ba5f4d7142505fc4e32290b --- /dev/null +++ b/crates/language_tools2/src/lsp_log.rs @@ -0,0 +1,1023 @@ +use collections::{HashMap, VecDeque}; +use editor::{Editor, MoveToEnd}; +use futures::{channel::mpsc, StreamExt}; +use gpui::{ + actions, + elements::{ + AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, + ParentElement, Stack, + }, + platform::{CursorStyle, MouseButton}, + AnyElement, AppContext, Element, Entity, Model, ModelContext, Subscription, View, View, + ViewContext, WeakModel, +}; +use language::{LanguageServerId, LanguageServerName}; +use lsp::IoKind; +use project::{search::SearchQuery, Project}; +use std::{borrow::Cow, sync::Arc}; +use theme::{ui, Theme}; +use workspace::{ + item::{Item, ItemHandle}, + searchable::{SearchableItem, SearchableItemHandle}, + ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated, +}; + +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, + _log_store_subscriptions: Vec, +} + +pub struct LspLogToolbarItemView { + log_view: Option>, + _log_view_subscription: Option, + menu_open: bool, +} + +#[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.add_model(|cx| LogStore::new(cx)); + + cx.subscribe_global::({ + let log_store = log_store.clone(); + move |event, cx| { + let workspace = &event.0; + if let Some(workspace) = workspace.upgrade(cx) { + let project = workspace.read(cx).project().clone(); + if project.read(cx).is_local() { + log_store.update(cx, |store, cx| { + store.add_project(&project, cx); + }); + } + } + } + }) + .detach(); + + cx.add_action( + move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| { + let project = workspace.project().read(cx); + if project.is_local() { + workspace.add_item( + Box::new(cx.add_view(|cx| { + LspLogView::new(workspace.project().clone(), log_store.clone(), cx) + })), + cx, + ); + } + }, + ); +} + +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_weak(|this, mut cx| async move { + while let Some((project, server_id, io_kind, message)) = io_rx.next().await { + if let Some(this) = this.upgrade(&cx) { + this.update(&mut cx, |this, cx| { + this.on_io(project, server_id, io_kind, &message, cx); + }); + } + } + anyhow::Ok(()) + }) + .detach(); + this + } + + pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { + let weak_project = project.downgrade(); + self.projects.insert( + weak_project, + 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, id, io_kind, message.to_string())) + .ok(); + }) + }); + let this = cx.weak_handle(); + 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(&mut cx).zip(this.upgrade(&mut cx)) + { + this.update(&mut cx, |this, cx| { + this.add_language_server_log(&project, server_id, ¶ms.message, cx); + }); + } + } + }) + }); + 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(cx)?; + 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 mut this = Self { + editor, + editor_subscription, + project, + log_store, + current_server_id: None, + is_showing_rpc_trace: false, + _log_store_subscriptions: vec![model_changes_subscription, events_subscriptions], + }; + 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.add_view(|cx| { + let mut editor = Editor::multi_line(None, 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, cx| 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(); + } + } + + 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_weak({ + 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(); + }); + + self.editor = editor; + self.editor_subscription = editor_subscription; + cx.notify(); + } + } + + 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 View for LspLogView { + fn ui_name() -> &'static str { + "LspLogView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + ChildView::new(&self.editor, cx).into_any() + } + + fn focus_in(&mut self, _: gpui::AnyView, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.editor); + } + } +} + +impl Item for LspLogView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> AnyElement { + Label::new("LSP Logs", style.label.clone()).into_any() + } + + fn as_searchable(&self, handle: &View) -> Option> { + Some(Box::new(handle.clone())) + } +} + +impl SearchableItem for LspLogView { + type Match = ::Match; + + fn to_search_event( + &mut self, + event: &Self::Event, + cx: &mut ViewContext, + ) -> Option { + self.editor + .update(cx, |editor, cx| editor.to_search_event(event, cx)) + } + + 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 ToolbarItemView for LspLogToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + self.menu_open = false; + 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 { + flex: Some((1., false)), + }; + } + } + self.log_view = None; + self._log_view_subscription = None; + ToolbarItemLocation::Hidden + } +} + +impl View for LspLogToolbarItemView { + fn ui_name() -> &'static str { + "LspLogView" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = theme::current(cx).clone(); + let Some(log_view) = self.log_view.as_ref() else { + return Empty::new().into_any(); + }; + 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 server_selected = current_server.is_some(); + + enum LspLogScroll {} + enum Menu {} + let lsp_menu = Stack::new() + .with_child(Self::render_language_server_menu_header( + current_server, + &theme, + cx, + )) + .with_children(if self.menu_open { + Some( + Overlay::new( + MouseEventHandler::new::(0, cx, move |_, cx| { + Flex::column() + .scrollable::(0, None, cx) + .with_children(menu_rows.into_iter().map(|row| { + Self::render_language_server_menu_item( + row.server_id, + row.server_name, + &row.worktree_root_name, + row.rpc_trace_enabled, + row.logs_selected, + row.rpc_trace_selected, + &theme, + cx, + ) + })) + .contained() + .with_style(theme.toolbar_dropdown_menu.container) + .constrained() + .with_width(400.) + .with_height(400.) + }) + .on_down_out(MouseButton::Left, |_, this, cx| { + this.menu_open = false; + cx.notify() + }), + ) + .with_hoverable(true) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .aligned() + .bottom() + .left(), + ) + } else { + None + }) + .aligned() + .left() + .clipped(); + + enum LspCleanupButton {} + let log_cleanup_button = + MouseEventHandler::new::(1, cx, |state, cx| { + let theme = theme::current(cx).clone(); + let style = theme + .workspace + .toolbar + .toggleable_text_tool + .in_state(server_selected) + .style_for(state); + Label::new("Clear", style.text.clone()) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0) + }) + .on_click(MouseButton::Left, move |_, 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); + }); + }) + } + }) + .with_cursor_style(CursorStyle::PointingHand) + .aligned() + .right(); + + Flex::row() + .with_child(lsp_menu) + .with_child(log_cleanup_button) + .contained() + .aligned() + .left() + .into_any_named("lsp log controls") + } +} + +const RPC_MESSAGES: &str = "RPC Messages"; +const SERVER_LOGS: &str = "Server Logs"; + +impl LspLogToolbarItemView { + pub fn new() -> Self { + Self { + menu_open: false, + log_view: None, + _log_view_subscription: None, + } + } + + fn toggle_menu(&mut self, cx: &mut ViewContext) { + self.menu_open = !self.menu_open; + cx.notify(); + } + + 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.notify(); + } + + fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx)); + self.menu_open = false; + cx.notify(); + } + } + + fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { + if let Some(log_view) = &self.log_view { + log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx)); + self.menu_open = false; + cx.notify(); + } + } + + fn render_language_server_menu_header( + current_server: Option, + theme: &Arc, + cx: &mut ViewContext, + ) -> impl Element { + enum ToggleMenu {} + MouseEventHandler::new::(0, cx, move |state, _| { + let label: Cow = current_server + .and_then(|row| { + Some( + format!( + "{} ({}) - {}", + row.server_name.0, + row.worktree_root_name, + if row.rpc_trace_selected { + RPC_MESSAGES + } else { + SERVER_LOGS + }, + ) + .into(), + ) + }) + .unwrap_or_else(|| "No server selected".into()); + let style = theme.toolbar_dropdown_menu.header.style_for(state); + Label::new(label, style.text.clone()) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.toggle_menu(cx); + }) + } + + fn render_language_server_menu_item( + id: LanguageServerId, + name: LanguageServerName, + worktree_root_name: &str, + rpc_trace_enabled: bool, + logs_selected: bool, + rpc_trace_selected: bool, + theme: &Arc, + cx: &mut ViewContext, + ) -> impl Element { + enum ActivateLog {} + enum ActivateRpcTrace {} + enum LanguageServerCheckbox {} + + Flex::column() + .with_child({ + let style = &theme.toolbar_dropdown_menu.section_header; + Label::new( + format!("{} ({})", name.0, worktree_root_name), + style.text.clone(), + ) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.toolbar_dropdown_menu.row_height) + }) + .with_child( + MouseEventHandler::new::(id.0, cx, move |state, _| { + let style = theme + .toolbar_dropdown_menu + .item + .in_state(logs_selected) + .style_for(state); + Label::new(SERVER_LOGS, style.text.clone()) + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.toolbar_dropdown_menu.row_height) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.show_logs_for_server(id, cx); + }), + ) + .with_child( + MouseEventHandler::new::(id.0, cx, move |state, cx| { + let style = theme + .toolbar_dropdown_menu + .item + .in_state(rpc_trace_selected) + .style_for(state); + Flex::row() + .with_child( + Label::new(RPC_MESSAGES, style.text.clone()) + .constrained() + .with_height(theme.toolbar_dropdown_menu.row_height), + ) + .with_child( + ui::checkbox_with_label::( + Empty::new(), + &theme.welcome.checkbox, + rpc_trace_enabled, + id.0, + cx, + move |this, enabled, cx| { + this.toggle_logging_for_server(id, enabled, cx); + }, + ) + .flex_float(), + ) + .align_children_center() + .contained() + .with_style(style.container) + .constrained() + .with_height(theme.toolbar_dropdown_menu.row_height) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.show_rpc_trace_for_server(id, cx); + }), + ) + } +} + +pub enum Event { + NewServerLogEntry { + id: LanguageServerId, + entry: String, + is_rpc: bool, + }, +} + +impl Entity for LogStore { + type Event = Event; +} + +impl Entity for LspLogView { + type Event = editor::Event; +} + +impl Entity for LspLogToolbarItemView { + type Event = (); +} 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..967e8a33824d75152472f3b4614b94c7a6d1671b --- /dev/null +++ b/crates/language_tools2/src/lsp_log_tests.rs @@ -0,0 +1,109 @@ +// todo!("TODO kb") +// use std::sync::Arc; + +// use crate::lsp_log::LogMenuItem; + +// use super::*; +// use futures::StreamExt; +// use gpui::{serde_json::json, TestAppContext}; +// 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()); +// 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.add_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 log_view = cx +// .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)) +// .root(cx); + +// language_server.notify::(lsp::LogMessageParams { +// message: "hello from the server".into(), +// typ: lsp::MessageType::INFO, +// }); +// cx.foreground().run_until_parked(); + +// log_view.read_with(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(cx) +// .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.foreground().forbid_parking(); + +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// theme::init((), 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..7eaaab013002b26938732010c2ebb256e44a6cc2 --- /dev/null +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -0,0 +1,677 @@ +use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; +use gpui::{ + actions, + elements::{ + AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, + ParentElement, ScrollTarget, Stack, UniformList, UniformListState, + }, + fonts::TextStyle, + AppContext, CursorStyle, Element, Entity, Model, MouseButton, View, View, ViewContext, + WeakView, +}; +use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +use std::{mem, ops::Range, sync::Arc}; +use theme::{Theme, ThemeSettings}; +use tree_sitter::{Node, TreeCursor}; +use workspace::{ + item::{Item, ItemHandle}, + ToolbarItemLocation, ToolbarItemView, Workspace, +}; + +actions!(debug, [OpenSyntaxTreeView]); + +pub fn init(cx: &mut AppContext) { + cx.add_action( + move |workspace: &mut 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.add_item(Box::new(syntax_tree_view), cx); + }, + ); +} + +pub struct SyntaxTreeView { + workspace_handle: WeakView, + editor: Option, + mouse_y: Option, + line_height: Option, + list_state: UniformListState, + selected_descendant_ix: Option, + hovered_descendant_ix: Option, +} + +pub struct SyntaxTreeToolbarItemView { + tree_view: Option>, + subscription: Option, + menu_open: bool, +} + +struct EditorState { + editor: ViewHandle, + 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_state: UniformListState::default(), + editor: None, + mouse_y: None, + line_height: None, + hovered_descendant_ix: None, + selected_descendant_ix: None, + }; + + this.workspace_updated(active_item, cx); + cx.observe( + &workspace_handle.upgrade(cx).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.id() != cx.view_id() { + if let Some(editor) = item.act_as::(cx) { + self.set_editor(editor, cx); + } + } + } + } + + fn set_editor(&mut self, editor: ViewHandle, 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::Event::Reparsed => true, + editor::Event::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_state.scroll_to(ScrollTarget::Show(descendant_ix)); + + cx.notify(); + Some(()) + } + + fn handle_click(&mut self, y: f32, cx: &mut ViewContext) -> Option<()> { + let line_height = self.line_height?; + let ix = ((self.list_state.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_state.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, + hovered: bool, + list_hovered: bool, + style: &TextStyle, + editor_theme: &theme::Editor, + cx: &AppContext, + ) -> gpui::AnyElement { + let node = cursor.node(); + let mut range_style = style.clone(); + let em_width = style.em_width(cx.font_cache()); + let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); + + range_style.color = editor_theme.line_number; + + let mut anonymous_node_style = style.clone(); + let string_color = editor_theme + .syntax + .highlights + .iter() + .find_map(|(name, style)| (name == "string").then(|| style.color)?); + let property_color = editor_theme + .syntax + .highlights + .iter() + .find_map(|(name, style)| (name == "property").then(|| style.color)?); + if let Some(color) = string_color { + anonymous_node_style.color = color; + } + + let mut row = Flex::row(); + if let Some(field_name) = cursor.field_name() { + let mut field_style = style.clone(); + if let Some(color) = property_color { + field_style.color = color; + } + + row.add_children([ + Label::new(field_name, field_style), + Label::new(": ", style.clone()), + ]); + } + + return row + .with_child( + if node.is_named() { + Label::new(node.kind(), style.clone()) + } else { + Label::new(format!("\"{}\"", node.kind()), anonymous_node_style) + } + .contained() + .with_margin_right(em_width), + ) + .with_child(Label::new(format_node_range(node), range_style)) + .contained() + .with_background_color(if selected { + editor_theme.selection.selection + } else if hovered && list_hovered { + editor_theme.active_line_background + } else { + Default::default() + }) + .with_padding_left(gutter_padding + depth as f32 * 18.0) + .into_any(); + } +} + +impl Entity for SyntaxTreeView { + type Event = (); +} + +impl View for SyntaxTreeView { + fn ui_name() -> &'static str { + "SyntaxTreeView" + } + + fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { + let settings = settings::get::(cx); + let font_family_id = settings.buffer_font_family; + let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); + let font_properties = Default::default(); + let font_id = cx + .font_cache() + .select_font(font_family_id, &font_properties) + .unwrap(); + let font_size = settings.buffer_font_size(cx); + + let editor_theme = settings.theme.editor.clone(); + let style = TextStyle { + color: editor_theme.text_color, + font_family_name, + font_family_id, + font_id, + font_size, + font_properties: Default::default(), + underline: Default::default(), + soft_wrap: false, + }; + + let line_height = cx.font_cache().line_height(font_size); + if Some(line_height) != self.line_height { + self.line_height = Some(line_height); + self.hover_state_changed(cx); + } + + 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 theme = editor_theme.clone(); + return MouseEventHandler::new::(0, cx, move |state, cx| { + let list_hovered = state.hovered(); + UniformList::new( + self.list_state.clone(), + layer.node().descendant_count(), + cx, + move |this, range, items, cx| { + 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, + Some(descendant_ix) == this.hovered_descendant_ix, + list_hovered, + &style, + &theme, + cx, + )); + descendant_ix += 1; + if cursor.goto_first_child() { + depth += 1; + } else { + visited_children = true; + } + } + } + }, + ) + }) + .on_move(move |event, this, cx| { + let y = event.position.y() - event.region.origin_y(); + this.mouse_y = Some(y); + this.hover_state_changed(cx); + }) + .on_click(MouseButton::Left, move |event, this, cx| { + let y = event.position.y() - event.region.origin_y(); + this.handle_click(y, cx); + }) + .contained() + .with_background_color(editor_theme.background) + .into_any(); + } + + Empty::new().into_any() + } +} + +impl Item for SyntaxTreeView { + fn tab_content( + &self, + _: Option, + style: &theme::Tab, + _: &AppContext, + ) -> gpui::AnyElement { + Label::new("Syntax Tree", style.label.clone()).into_any() + } + + fn clone_on_split( + &self, + _workspace_id: workspace::WorkspaceId, + cx: &mut ViewContext, + ) -> Option + where + Self: Sized, + { + let mut clone = Self::new(self.workspace_handle.clone(), None, cx); + if let Some(editor) = &self.editor { + clone.set_editor(editor.editor.clone(), cx) + } + Some(clone) + } +} + +impl SyntaxTreeToolbarItemView { + pub fn new() -> Self { + Self { + menu_open: false, + tree_view: None, + subscription: None, + } + } + + fn render_menu( + &mut self, + cx: &mut ViewContext<'_, '_, Self>, + ) -> Option> { + let theme = theme::current(cx).clone(); + 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(); + + enum Menu {} + + Some( + Stack::new() + .with_child(Self::render_header(&theme, &active_layer, cx)) + .with_children(self.menu_open.then(|| { + Overlay::new( + MouseEventHandler::new::(0, cx, move |_, cx| { + Flex::column() + .with_children(active_buffer.syntax_layers().enumerate().map( + |(ix, layer)| { + Self::render_menu_item(&theme, &active_layer, layer, ix, cx) + }, + )) + .contained() + .with_style(theme.toolbar_dropdown_menu.container) + .constrained() + .with_width(400.) + .with_height(400.) + }) + .on_down_out(MouseButton::Left, |_, this, cx| { + this.menu_open = false; + cx.notify() + }), + ) + .with_hoverable(true) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .aligned() + .bottom() + .left() + })) + .aligned() + .left() + .clipped() + .into_any(), + ) + } + + fn toggle_menu(&mut self, cx: &mut ViewContext) { + self.menu_open = !self.menu_open; + cx.notify(); + } + + 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; + self.menu_open = false; + cx.notify(); + Some(()) + }) + } + + fn render_header( + theme: &Arc, + active_layer: &OwnedSyntaxLayerInfo, + cx: &mut ViewContext, + ) -> impl Element { + enum ToggleMenu {} + MouseEventHandler::new::(0, cx, move |state, _| { + let style = theme.toolbar_dropdown_menu.header.style_for(state); + Flex::row() + .with_child( + Label::new(active_layer.language.name().to_string(), style.text.clone()) + .contained() + .with_margin_right(style.secondary_text_spacing), + ) + .with_child(Label::new( + format_node_range(active_layer.node()), + style + .secondary_text + .clone() + .unwrap_or_else(|| style.text.clone()), + )) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.toggle_menu(cx); + }) + } + + fn render_menu_item( + theme: &Arc, + active_layer: &OwnedSyntaxLayerInfo, + layer: SyntaxLayerInfo, + layer_ix: usize, + cx: &mut ViewContext, + ) -> impl Element { + enum ActivateLayer {} + MouseEventHandler::new::(layer_ix, cx, move |state, _| { + let is_selected = layer.node() == active_layer.node(); + let style = theme + .toolbar_dropdown_menu + .item + .in_state(is_selected) + .style_for(state); + Flex::row() + .with_child( + Label::new(layer.language.name().to_string(), style.text.clone()) + .contained() + .with_margin_right(style.secondary_text_spacing), + ) + .with_child(Label::new( + format_node_range(layer.node()), + style + .secondary_text + .clone() + .unwrap_or_else(|| style.text.clone()), + )) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, view, cx| { + view.select_layer(layer_ix, cx); + }) + } +} + +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 Entity for SyntaxTreeToolbarItemView { + type Event = (); +} + +impl View for SyntaxTreeToolbarItemView { + fn ui_name() -> &'static str { + "SyntaxTreeToolbarItemView" + } + + fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement { + self.render_menu(cx) + .unwrap_or_else(|| Empty::new().into_any()) + } +} + +impl ToolbarItemView for SyntaxTreeToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) -> workspace::ToolbarItemLocation { + self.menu_open = false; + 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 { + flex: Some((1., false)), + }; + } + } + self.tree_view = None; + self.subscription = None; + ToolbarItemLocation::Hidden + } +} From 676a07270e24dfdce4c6445308efe5f694bbc0f7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 11:17:02 +0200 Subject: [PATCH 02/14] First fixes --- crates/language_tools2/src/lsp_log.rs | 56 ++++++-------- .../language_tools2/src/syntax_tree_view.rs | 73 ++++++++----------- 2 files changed, 52 insertions(+), 77 deletions(-) diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 9f27c62a04a001a15ba5f4d7142505fc4e32290b..6e433ba8a64e0bcd9469bf96306b81c3025e947b 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -2,24 +2,19 @@ use collections::{HashMap, VecDeque}; use editor::{Editor, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, - elements::{ - AnchorCorner, ChildView, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, - ParentElement, Stack, - }, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Element, Entity, Model, ModelContext, Subscription, View, View, - ViewContext, WeakModel, + actions, AnchorCorner, AnyElement, AppContext, Context, CursorStyle, Element, Empty, Entity, + Model, ModelContext, MouseButton, Overlay, OverlayFitMode, Subscription, View, ViewContext, + VisualContext, WeakModel, }; use language::{LanguageServerId, LanguageServerName}; use lsp::IoKind; use project::{search::SearchQuery, Project}; use std::{borrow::Cow, sync::Arc}; -use theme::{ui, Theme}; +use theme::Theme; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchableItem, SearchableItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, WorkspaceCreated, + ToolbarItemLocation, ToolbarItemView, Workspace, }; const SEND_LINE: &str = "// Send:"; @@ -83,37 +78,29 @@ pub(crate) struct LogMenuItem { actions!(debug, [OpenLanguageServerLogs]); pub fn init(cx: &mut AppContext) { - let log_store = cx.add_model(|cx| LogStore::new(cx)); - - cx.subscribe_global::({ - let log_store = log_store.clone(); - move |event, cx| { - let workspace = &event.0; - if let Some(workspace) = workspace.upgrade(cx) { - let project = workspace.read(cx).project().clone(); - if project.read(cx).is_local() { - log_store.update(cx, |store, cx| { - store.add_project(&project, cx); - }); - } - } + let log_store = cx.build_model(|cx| LogStore::new(cx)); + + cx.observe_new_views(|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); + }); } - }) - .detach(); - cx.add_action( - move |workspace: &mut Workspace, _: &OpenLanguageServerLogs, cx: _| { + workspace.register_action(|workspace, _: &OpenLanguageServerLogs, cx| { let project = workspace.project().read(cx); if project.is_local() { workspace.add_item( - Box::new(cx.add_view(|cx| { + Box::new(cx.build_view(|cx| { LspLogView::new(workspace.project().clone(), log_store.clone(), cx) })), cx, ); } - }, - ); + }); + }) + .detach(); } impl LogStore { @@ -204,8 +191,7 @@ impl LogStore { let server_id = server.server_id(); server.on_notification::({ move |params, mut cx| { - if let Some((project, this)) = - weak_project.upgrade(&mut cx).zip(this.upgrade(&mut cx)) + if let Some((project, this)) = weak_project.upgrade().zip(this.upgrade(&mut cx)) { this.update(&mut cx, |this, cx| { this.add_language_server_log(&project, server_id, ¶ms.message, cx); @@ -314,7 +300,7 @@ impl LogStore { IoKind::StdOut => true, IoKind::StdIn => false, IoKind::StdErr => { - let project = project.upgrade(cx)?; + let project = project.upgrade()?; let message = format!("stderr: {}", message.trim()); self.add_language_server_log(&project, language_server_id, &message, cx); return Some(()); @@ -446,7 +432,7 @@ impl LspLogView { log_contents: String, cx: &mut ViewContext, ) -> (View, Subscription) { - let editor = cx.add_view(|cx| { + let editor = cx.build_view(|cx| { let mut editor = Editor::multi_line(None, cx); editor.set_text(log_contents, cx); editor.move_to_end(&MoveToEnd, cx); diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 7eaaab013002b26938732010c2ebb256e44a6cc2..80ff211176598539538bd3d8cd490feb7f2ef5cd 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -1,35 +1,32 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ - actions, - elements::{ - AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, - ParentElement, ScrollTarget, Stack, UniformList, UniformListState, - }, - fonts::TextStyle, - AppContext, CursorStyle, Element, Entity, Model, MouseButton, View, View, ViewContext, - WeakView, + actions, AnchorCorner, AppContext, CursorStyle, Div, Element, Empty, Entity, Focusable, Model, + MouseButton, Overlay, OverlayFitMode, ParentElement, Render, TextStyle, UniformList, + UniformListState, View, ViewContext, VisualContext, WeakView, }; use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; use std::{mem, ops::Range, sync::Arc}; -use theme::{Theme, ThemeSettings}; +use theme::{ActiveTheme, Theme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; use workspace::{ item::{Item, ItemHandle}, + ui::v_stack, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(debug, [OpenSyntaxTreeView]); pub fn init(cx: &mut AppContext) { - cx.add_action( - move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| { + 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.add_item(Box::new(syntax_tree_view), cx); - }, - ); + }); + }) + .detach(); } pub struct SyntaxTreeView { @@ -49,7 +46,7 @@ pub struct SyntaxTreeToolbarItemView { } struct EditorState { - editor: ViewHandle, + editor: View, active_buffer: Option, _subscription: gpui::Subscription, } @@ -79,7 +76,7 @@ impl SyntaxTreeView { this.workspace_updated(active_item, cx); cx.observe( - &workspace_handle.upgrade(cx).unwrap(), + &workspace_handle.upgrade().unwrap(), |this, workspace, cx| { this.workspace_updated(workspace.read(cx).active_item(cx), cx); }, @@ -95,7 +92,7 @@ impl SyntaxTreeView { cx: &mut ViewContext, ) { if let Some(item) = active_item { - if item.id() != cx.view_id() { + if item.item_id() != cx.entity_id() { if let Some(editor) = item.act_as::(cx) { self.set_editor(editor, cx); } @@ -103,7 +100,7 @@ impl SyntaxTreeView { } } - fn set_editor(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + fn set_editor(&mut self, editor: View, cx: &mut ViewContext) { if let Some(state) = &self.editor { if state.editor == editor { return; @@ -115,8 +112,8 @@ impl SyntaxTreeView { let subscription = cx.subscribe(&editor, |this, _, event, cx| { let did_reparse = match event { - editor::Event::Reparsed => true, - editor::Event::SelectionsChanged { .. } => false, + editor::EditorEvent::Reparsed => true, + editor::EditorEvent::SelectionsChanged { .. } => false, _ => return, }; this.editor_updated(did_reparse, cx); @@ -490,7 +487,7 @@ impl SyntaxTreeToolbarItemView { &mut self, cx: &mut ViewContext<'_, '_, Self>, ) -> Option> { - let theme = theme::current(cx).clone(); + let theme = cx.theme().clone(); let tree_view = self.tree_view.as_ref()?; let tree_view = tree_view.read(cx); @@ -502,12 +499,12 @@ impl SyntaxTreeToolbarItemView { enum Menu {} Some( - Stack::new() - .with_child(Self::render_header(&theme, &active_layer, cx)) - .with_children(self.menu_open.then(|| { - Overlay::new( - MouseEventHandler::new::(0, cx, move |_, cx| { - Flex::column() + v_stack() + .child(Self::render_header(&theme, &active_layer, cx)) + .children(self.menu_open.then(|| { + overlay( + mouse_event_handler::(0, cx, move |_, cx| { + v_stack() .with_children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| { Self::render_menu_item(&theme, &active_layer, layer, ix, cx) @@ -525,16 +522,9 @@ impl SyntaxTreeToolbarItemView { }), ) .with_hoverable(true) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopLeft) - .with_z_index(999) - .aligned() - .bottom() - .left() + .with_fit_content() + .into_any() })) - .aligned() - .left() - .clipped() .into_any(), ) } @@ -639,14 +629,13 @@ fn format_node_range(node: Node) -> String { ) } -impl Entity for SyntaxTreeToolbarItemView { - type Event = (); -} +impl Render for SyntaxTreeToolbarItemView { + type Element = Focusable
; -impl View for SyntaxTreeToolbarItemView { - fn ui_name() -> &'static str { - "SyntaxTreeToolbarItemView" - } + // todo!() + // fn ui_name() -> &'static str { + // "SyntaxTreeToolbarItemView" + // } fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement { self.render_menu(cx) From 75074c329792d72ac53b2db0ae13c56e345d1466 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 15:32:09 +0200 Subject: [PATCH 03/14] Apply more fixes to the visual part --- Cargo.lock | 1 + crates/editor2/src/editor.rs | 13 +- crates/language_tools2/Cargo.toml | 1 + crates/language_tools2/src/lsp_log.rs | 388 ++++++++---------- .../language_tools2/src/syntax_tree_view.rs | 361 +++++++--------- 5 files changed, 335 insertions(+), 429 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4f418a1813837cdb60f325305aa223a4b1a8a41e..7237ab6905db5a5aadfa1c4613114c3f12ae6bca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5008,6 +5008,7 @@ dependencies = [ "settings2", "theme2", "tree-sitter", + "ui2", "unindent", "util", "workspace2", 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/language_tools2/Cargo.toml b/crates/language_tools2/Cargo.toml index ca3ede2ef9a1e80e32afedec15bf8c0bdf02afbc..bf5202cda9959556a5d225c8b1ce01591d7c8eac 100644 --- a/crates/language_tools2/Cargo.toml +++ b/crates/language_tools2/Cargo.toml @@ -17,6 +17,7 @@ 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 diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 6e433ba8a64e0bcd9469bf96306b81c3025e947b..ca4f25e177fc3204ea155600d184d76c50bdfa08 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -1,20 +1,22 @@ use collections::{HashMap, VecDeque}; -use editor::{Editor, MoveToEnd}; +use editor::{Editor, EditorElement, EditorEvent, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, AnchorCorner, AnyElement, AppContext, Context, CursorStyle, Element, Empty, Entity, - Model, ModelContext, MouseButton, Overlay, OverlayFitMode, Subscription, View, ViewContext, - VisualContext, WeakModel, + actions, div, overlay, red, AnchorCorner, AnyElement, AppContext, Context, CursorStyle, Div, + EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, + MouseButton, OverlayFitMode, 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 theme::Theme; +use theme::{ActiveTheme, Theme}; +use ui::{h_stack, v_stack, Label}; use workspace::{ item::{Item, ItemHandle}, - searchable::{SearchableItem, SearchableItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; const SEND_LINE: &str = "// Send:"; @@ -50,6 +52,7 @@ pub struct LspLogView { current_server_id: Option, is_showing_rpc_trace: bool, project: Model, + focus_handle: FocusHandle, _log_store_subscriptions: Vec, } @@ -110,9 +113,9 @@ impl LogStore { projects: HashMap::default(), io_tx, }; - cx.spawn_weak(|this, mut cx| async move { + 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(&cx) { + if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { this.on_io(project, server_id, io_kind, &message, cx); }); @@ -120,7 +123,7 @@ impl LogStore { } anyhow::Ok(()) }) - .detach(); + .detach_and_log_err(cx); this } @@ -131,7 +134,7 @@ impl LogStore { ProjectState { servers: HashMap::default(), _subscriptions: [ - cx.observe_release(&project, move |this, _, _| { + cx.observe_release(project, move |this, _, _| { this.projects.remove(&weak_project); }), cx.subscribe(project, |this, project, event, cx| match event { @@ -185,14 +188,13 @@ impl LogStore { .ok(); }) }); - let this = cx.weak_handle(); + 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(&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); }); @@ -413,14 +415,25 @@ impl LspLogView { } }); 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], + _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); @@ -433,13 +446,18 @@ impl LspLogView { cx: &mut ViewContext, ) -> (View, Subscription) { let editor = cx.build_view(|cx| { - let mut editor = Editor::multi_line(None, 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, cx| cx.emit(event.clone())); + let editor_subscription = cx.subscribe( + &editor, + |_, _, event: &EditorEvent, cx: &mut ViewContext<'_, LspLogView>| { + cx.emit(event.clone()) + }, + ); (editor, editor_subscription) } @@ -526,7 +544,7 @@ impl LspLogView { .as_singleton() .expect("log buffer should be a singleton") .update(cx, |_, cx| { - cx.spawn_weak({ + cx.spawn({ let buffer = cx.handle(); |_, mut cx| async move { let language = language.await.ok(); @@ -574,30 +592,34 @@ fn log_contents(lines: &VecDeque) -> String { } } -impl View for LspLogView { - fn ui_name() -> &'static str { - "LspLogView" - } +impl Render for LspLogView { + // todo!() + // fn ui_name() -> &'static str { + // "LspLogView" + // } - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - ChildView::new(&self.editor, cx).into_any() + type Element = EditorElement; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + self.editor.update(cx, |editor, cx| editor.render(cx)) } +} - fn focus_in(&mut self, _: gpui::AnyView, cx: &mut ViewContext) { - if cx.is_self_focused() { - cx.focus(&self.editor); - } +impl FocusableView for LspLogView { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } impl Item for LspLogView { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> AnyElement { - Label::new("LSP Logs", style.label.clone()).into_any() + 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> { @@ -608,15 +630,6 @@ impl Item for LspLogView { impl SearchableItem for LspLogView { type Match = ::Match; - fn to_search_event( - &mut self, - event: &Self::Event, - cx: &mut ViewContext, - ) -> Option { - self.editor - .update(cx, |editor, cx| editor.to_search_event(event, cx)) - } - fn clear_matches(&mut self, cx: &mut ViewContext) { self.editor.update(cx, |e, cx| e.clear_matches(cx)) } @@ -675,6 +688,8 @@ impl SearchableItem for LspLogView { } } +impl EventEmitter for LspLogToolbarItemView {} + impl ToolbarItemView for LspLogToolbarItemView { fn set_active_pane_item( &mut self, @@ -688,9 +703,7 @@ impl ToolbarItemView for LspLogToolbarItemView { self._log_view_subscription = Some(cx.observe(&log_view, |_, _, cx| { cx.notify(); })); - return ToolbarItemLocation::PrimaryLeft { - flex: Some((1., false)), - }; + return ToolbarItemLocation::PrimaryLeft; } } self.log_view = None; @@ -699,15 +712,17 @@ impl ToolbarItemView for LspLogToolbarItemView { } } -impl View for LspLogToolbarItemView { - fn ui_name() -> &'static str { - "LspLogView" - } +impl Render for LspLogToolbarItemView { + type Element = Div; + // todo!() + // fn ui_name() -> &'static str { + // "LspLogView" + // } - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx).clone(); + fn render(&mut self, cx: &mut ViewContext) -> Div { + let theme = cx.theme().clone(); let Some(log_view) = self.log_view.as_ref() else { - return Empty::new().into_any(); + 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(); @@ -726,19 +741,15 @@ impl View for LspLogToolbarItemView { enum LspLogScroll {} enum Menu {} - let lsp_menu = Stack::new() - .with_child(Self::render_language_server_menu_header( - current_server, - &theme, - cx, - )) - .with_children(if self.menu_open { + let lsp_menu = h_stack() + .child(Self::render_language_server_menu_header(current_server, cx)) + .children(if self.menu_open { Some( - Overlay::new( - MouseEventHandler::new::(0, cx, move |_, cx| { - Flex::column() + overlay() + .child( + v_stack() .scrollable::(0, None, cx) - .with_children(menu_rows.into_iter().map(|row| { + .children(menu_rows.into_iter().map(|row| { Self::render_language_server_menu_item( row.server_id, row.server_name, @@ -750,51 +761,27 @@ impl View for LspLogToolbarItemView { cx, ) })) - .contained() - .with_style(theme.toolbar_dropdown_menu.container) - .constrained() - .with_width(400.) - .with_height(400.) - }) - .on_down_out(MouseButton::Left, |_, this, cx| { - this.menu_open = false; - cx.notify() - }), - ) - .with_hoverable(true) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopLeft) - .with_z_index(999) - .aligned() - .bottom() - .left(), + .on_down_out(MouseButton::Left, |_, this, cx| { + this.menu_open = false; + cx.notify() + }), + ) + .with_hoverable(true) + .with_fit_mode(OverlayFitMode::SwitchAnchor) + .with_anchor_corner(AnchorCorner::TopLeft) + .with_z_index(999) + .bottom() + .left(), ) } else { None - }) - .aligned() - .left() - .clipped(); + }); enum LspCleanupButton {} - let log_cleanup_button = - MouseEventHandler::new::(1, cx, |state, cx| { - let theme = theme::current(cx).clone(); - let style = theme - .workspace - .toolbar - .toggleable_text_tool - .in_state(server_selected) - .style_for(state); - Label::new("Clear", style.text.clone()) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height / 6.0 * 5.0) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(log_view) = this.log_view.as_ref() { + let log_cleanup_button = div() + .child(Label::new("Clear")) + .on_mouse_down(MouseButton::Left, move |_, cx| { + if let Some(log_view) = self.log_view.as_ref() { log_view.update(cx, |log_view, cx| { log_view.editor.update(cx, |editor, cx| { editor.set_read_only(false); @@ -804,17 +791,13 @@ impl View for LspLogToolbarItemView { }) } }) - .with_cursor_style(CursorStyle::PointingHand) - .aligned() - .right(); - - Flex::row() - .with_child(lsp_menu) - .with_child(log_cleanup_button) - .contained() - .aligned() - .left() - .into_any_named("lsp log controls") + .cursor(CursorStyle::PointingHand); + + h_stack() + .child(lsp_menu) + .child(log_cleanup_button) + .border_1() + .border_color(red()) } } @@ -871,37 +854,37 @@ impl LspLogToolbarItemView { fn render_language_server_menu_header( current_server: Option, - theme: &Arc, cx: &mut ViewContext, - ) -> impl Element { + ) -> Div { + let view = cx.view().clone(); enum ToggleMenu {} - MouseEventHandler::new::(0, cx, move |state, _| { - let label: Cow = current_server - .and_then(|row| { - Some( - format!( - "{} ({}) - {}", - row.server_name.0, - row.worktree_root_name, - if row.rpc_trace_selected { - RPC_MESSAGES - } else { - SERVER_LOGS - }, - ) - .into(), + let label: Cow = current_server + .and_then(|row| { + Some( + format!( + "{} ({}) - {}", + row.server_name.0, + row.worktree_root_name, + if row.rpc_trace_selected { + RPC_MESSAGES + } else { + SERVER_LOGS + }, ) + .into(), + ) + }) + .unwrap_or_else(|| "No server selected".into()); + div() + .child(Label::new(label)) + .cursor(CursorStyle::PointingHand) + .on_mouse_down(MouseButton::Left, move |_, cx| { + view.update(cx, |view, cx| { + view.toggle_menu(cx); }) - .unwrap_or_else(|| "No server selected".into()); - let style = theme.toolbar_dropdown_menu.header.style_for(state); - Label::new(label, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.toggle_menu(cx); - }) + }) + .border_1() + .border_color(red()) } fn render_language_server_menu_item( @@ -913,78 +896,52 @@ impl LspLogToolbarItemView { rpc_trace_selected: bool, theme: &Arc, cx: &mut ViewContext, - ) -> impl Element { + ) -> Div { enum ActivateLog {} enum ActivateRpcTrace {} enum LanguageServerCheckbox {} - Flex::column() - .with_child({ - let style = &theme.toolbar_dropdown_menu.section_header; - Label::new( - format!("{} ({})", name.0, worktree_root_name), - style.text.clone(), - ) - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height) - }) - .with_child( - MouseEventHandler::new::(id.0, cx, move |state, _| { - let style = theme - .toolbar_dropdown_menu - .item - .in_state(logs_selected) - .style_for(state); - Label::new(SERVER_LOGS, style.text.clone()) - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.show_logs_for_server(id, cx); - }), + let view = cx.view().clone(); + + v_stack() + .child(Label::new(format!("{} ({})", name.0, worktree_root_name))) + .child( + div() + .child(Label::new(SERVER_LOGS)) + .cursor(CursorStyle::PointingHand) + .on_mouse_down(MouseButton::Left, move |_, cx| { + view.update(cx, |view, cx| { + view.show_logs_for_server(id, cx); + }) + }), ) - .with_child( - MouseEventHandler::new::(id.0, cx, move |state, cx| { - let style = theme - .toolbar_dropdown_menu - .item - .in_state(rpc_trace_selected) - .style_for(state); - Flex::row() - .with_child( - Label::new(RPC_MESSAGES, style.text.clone()) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height), - ) - .with_child( - ui::checkbox_with_label::( - Empty::new(), - &theme.welcome.checkbox, - rpc_trace_enabled, - id.0, - cx, - move |this, enabled, cx| { - this.toggle_logging_for_server(id, enabled, cx); - }, - ) - .flex_float(), + .child( + h_stack() + .child(Label::new(RPC_MESSAGES)) + .child( + ui::checkbox_with_label::( + div(), + &theme.welcome.checkbox, + rpc_trace_enabled, + id.0, + cx, + move |this, enabled, cx| { + this.toggle_logging_for_server(id, enabled, cx); + }, ) - .align_children_center() - .contained() - .with_style(style.container) - .constrained() - .with_height(theme.toolbar_dropdown_menu.row_height) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.show_rpc_trace_for_server(id, cx); - }), + .flex_float(), + ) + .border_1() + .border_color(red()) + .cursor(CursorStyle::PointingHand) + .on_mouse_down(MouseButton::Left, move |_, cx| { + view.update(cx, |view, cx| { + view.show_rpc_trace_for_server(id, cx); + }) + }), ) + .border_1() + .border_color(red()) } } @@ -996,14 +953,7 @@ pub enum Event { }, } -impl Entity for LogStore { - type Event = Event; -} - -impl Entity for LspLogView { - type Event = editor::Event; -} - -impl Entity for LspLogToolbarItemView { - type Event = (); -} +impl EventEmitter for LogStore {} +impl EventEmitter for LspLogView {} +impl EventEmitter for LspLogView {} +impl EventEmitter for LspLogView {} diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 80ff211176598539538bd3d8cd490feb7f2ef5cd..7efd5f572323d08a8c2c9b9e675eeedee2fd5964 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -1,17 +1,20 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ - actions, AnchorCorner, AppContext, CursorStyle, Div, Element, Empty, Entity, Focusable, Model, - MouseButton, Overlay, OverlayFitMode, ParentElement, Render, TextStyle, UniformList, - UniformListState, View, ViewContext, VisualContext, WeakView, + actions, div, overlay, red, uniform_list, AnyElement, AppContext, CursorStyle, Div, + EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, + MouseButton, ParentElement, Render, Styled, TextStyle, UniformListState, View, ViewContext, + VisualContext, WeakView, WindowContext, }; use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; -use std::{mem, ops::Range, sync::Arc}; -use theme::{ActiveTheme, Theme, ThemeSettings}; +use settings::Settings; +use std::{mem, ops::Range}; +use theme::{ActiveTheme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; +use ui::{h_stack, Label}; use workspace::{ item::{Item, ItemHandle}, ui::v_stack, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(debug, [OpenSyntaxTreeView]); @@ -37,6 +40,7 @@ pub struct SyntaxTreeView { list_state: UniformListState, selected_descendant_ix: Option, hovered_descendant_ix: Option, + focus_handle: FocusHandle, } pub struct SyntaxTreeToolbarItemView { @@ -72,6 +76,7 @@ impl SyntaxTreeView { line_height: None, hovered_descendant_ix: None, selected_descendant_ix: None, + focus_handle: cx.focus_handle(), }; this.workspace_updated(active_item, cx); @@ -229,7 +234,7 @@ impl SyntaxTreeView { editor.clear_background_highlights::(cx); editor.highlight_background::( vec![range], - |theme| theme.editor.document_highlight_write_background, + |theme| theme.editor_document_highlight_write_background, cx, ); }); @@ -281,10 +286,10 @@ impl SyntaxTreeView { style: &TextStyle, editor_theme: &theme::Editor, cx: &AppContext, - ) -> gpui::AnyElement { + ) -> Div { let node = cursor.node(); let mut range_style = style.clone(); - let em_width = style.em_width(cx.font_cache()); + let em_width = style.em_width(cx.text_system()); let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); range_style.color = editor_theme.line_number; @@ -304,64 +309,54 @@ impl SyntaxTreeView { anonymous_node_style.color = color; } - let mut row = Flex::row(); + let mut row = h_stack(); if let Some(field_name) = cursor.field_name() { let mut field_style = style.clone(); if let Some(color) = property_color { field_style.color = color; } - row.add_children([ - Label::new(field_name, field_style), - Label::new(": ", style.clone()), - ]); + row = row.children([Label::new(field_name), Label::new(": ")]); } return row - .with_child( + .child( if node.is_named() { - Label::new(node.kind(), style.clone()) + Label::new(node.kind()) } else { - Label::new(format!("\"{}\"", node.kind()), anonymous_node_style) - } - .contained() - .with_margin_right(em_width), + Label::new(format!("\"{}\"", node.kind())) + }, // todo!() + // .margin(em_width), ) - .with_child(Label::new(format_node_range(node), range_style)) - .contained() - .with_background_color(if selected { + .child(Label::new(format_node_range(node))) + .text_bg(if selected { editor_theme.selection.selection } else if hovered && list_hovered { editor_theme.active_line_background } else { - Default::default() + Hsla::default() }) - .with_padding_left(gutter_padding + depth as f32 * 18.0) - .into_any(); + // todo!() + // .padding(gutter_padding + depth as f32 * 18.0) + .border_1() + .border_color(red()); } } -impl Entity for SyntaxTreeView { - type Event = (); -} +impl Render for SyntaxTreeView { + // todo!() + // fn ui_name() -> &'static str { + // "SyntaxTreeView" + // } -impl View for SyntaxTreeView { - fn ui_name() -> &'static str { - "SyntaxTreeView" - } + type Element = Div; - fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { - let settings = settings::get::(cx); - let font_family_id = settings.buffer_font_family; - let font_family_name = cx.font_cache().family_name(font_family_id).unwrap(); - let font_properties = Default::default(); - let font_id = cx - .font_cache() - .select_font(font_family_id, &font_properties) - .unwrap(); + fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Div { + let settings = ThemeSettings::get_global(cx); + let font = settings.buffer_font; let font_size = settings.buffer_font_size(cx); - let editor_theme = settings.theme.editor.clone(); + let editor_theme = settings.active_theme; let style = TextStyle { color: editor_theme.text_color, font_family_name, @@ -370,10 +365,16 @@ impl View for SyntaxTreeView { font_size, font_properties: Default::default(), underline: Default::default(), - soft_wrap: false, + font_family: todo!(), + font_features: todo!(), + line_height: todo!(), + font_weight: todo!(), + font_style: todo!(), + background_color: todo!(), + white_space: todo!(), }; - let line_height = cx.font_cache().line_height(font_size); + let line_height = cx.text_system().line_height(font_size); if Some(line_height) != self.line_height { self.line_height = Some(line_height); self.hover_state_changed(cx); @@ -387,90 +388,95 @@ impl View for SyntaxTreeView { { let layer = layer.clone(); let theme = editor_theme.clone(); - return MouseEventHandler::new::(0, cx, move |state, cx| { - let list_hovered = state.hovered(); - UniformList::new( - self.list_state.clone(), - layer.node().descendant_count(), - cx, - move |this, range, items, cx| { - 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; - } + + let list_hovered = state.hovered(); + uniform_list( + self.list_state.clone(), + layer.node().descendant_count(), + cx, + move |this, range, items, cx| { + 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 { - items.push(Self::render_node( - &cursor, - depth, - Some(descendant_ix) == this.selected_descendant_ix, - Some(descendant_ix) == this.hovered_descendant_ix, - list_hovered, - &style, - &theme, - cx, - )); - descendant_ix += 1; - if cursor.goto_first_child() { - depth += 1; - } else { - visited_children = true; - } + break; + } + } else { + items.push(Self::render_node( + &cursor, + depth, + Some(descendant_ix) == this.selected_descendant_ix, + Some(descendant_ix) == this.hovered_descendant_ix, + list_hovered, + &style, + &theme, + cx, + )); + descendant_ix += 1; + if cursor.goto_first_child() { + depth += 1; + } else { + visited_children = true; } } - }, - ) - }) + } + }, + ) .on_move(move |event, this, cx| { let y = event.position.y() - event.region.origin_y(); this.mouse_y = Some(y); this.hover_state_changed(cx); }) - .on_click(MouseButton::Left, move |event, this, cx| { + .on_mouse_down(MouseButton::Left, move |event, cx| { let y = event.position.y() - event.region.origin_y(); - this.handle_click(y, cx); - }) - .contained() - .with_background_color(editor_theme.background) - .into_any(); + self.handle_click(y, cx); + }); } - Empty::new().into_any() + div() + } +} + +impl EventEmitter<()> for SyntaxTreeView {} + +impl FocusableView for SyntaxTreeView { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() } } impl Item for SyntaxTreeView { - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> gpui::AnyElement { - Label::new("Syntax Tree", style.label.clone()).into_any() + 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_id: workspace::WorkspaceId, + _: workspace::WorkspaceId, cx: &mut ViewContext, - ) -> Option + ) -> Option> where Self: Sized, { - let mut clone = Self::new(self.workspace_handle.clone(), None, cx); - if let Some(editor) = &self.editor { - clone.set_editor(editor.editor.clone(), cx) - } - Some(clone) + 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 + })) } } @@ -483,10 +489,7 @@ impl SyntaxTreeToolbarItemView { } } - fn render_menu( - &mut self, - cx: &mut ViewContext<'_, '_, Self>, - ) -> Option> { + fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option
{ let theme = cx.theme().clone(); let tree_view = self.tree_view.as_ref()?; let tree_view = tree_view.read(cx); @@ -496,36 +499,23 @@ impl SyntaxTreeToolbarItemView { let active_layer = buffer_state.active_layer.clone()?; let active_buffer = buffer_state.buffer.read(cx).snapshot(); - enum Menu {} - Some( v_stack() - .child(Self::render_header(&theme, &active_layer, cx)) + .child(Self::render_header(&active_layer, cx)) .children(self.menu_open.then(|| { - overlay( - mouse_event_handler::(0, cx, move |_, cx| { - v_stack() - .with_children(active_buffer.syntax_layers().enumerate().map( - |(ix, layer)| { - Self::render_menu_item(&theme, &active_layer, layer, ix, cx) - }, - )) - .contained() - .with_style(theme.toolbar_dropdown_menu.container) - .constrained() - .with_width(400.) - .with_height(400.) - }) - .on_down_out(MouseButton::Left, |_, this, cx| { - this.menu_open = false; - cx.notify() - }), + overlay().child( + v_stack() + .children(active_buffer.syntax_layers().enumerate().map( + |(ix, layer)| Self::render_menu_item(&active_layer, layer, ix, cx), + )) + .on_mouse_down_out(|e, cx| { + if e.button == MouseButton::Left { + self.menu_open = false; + cx.notify() + } + }), ) - .with_hoverable(true) - .with_fit_content() - .into_any() - })) - .into_any(), + })), ) } @@ -549,71 +539,39 @@ impl SyntaxTreeToolbarItemView { }) } - fn render_header( - theme: &Arc, - active_layer: &OwnedSyntaxLayerInfo, - cx: &mut ViewContext, - ) -> impl Element { - enum ToggleMenu {} - MouseEventHandler::new::(0, cx, move |state, _| { - let style = theme.toolbar_dropdown_menu.header.style_for(state); - Flex::row() - .with_child( - Label::new(active_layer.language.name().to_string(), style.text.clone()) - .contained() - .with_margin_right(style.secondary_text_spacing), - ) - .with_child(Label::new( - format_node_range(active_layer.node()), - style - .secondary_text - .clone() - .unwrap_or_else(|| style.text.clone()), - )) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.toggle_menu(cx); - }) + fn render_header(active_layer: &OwnedSyntaxLayerInfo, cx: &mut ViewContext) -> Div { + let view = cx.view().clone(); + h_stack() + .child(Label::new(active_layer.language.name())) + .child(Label::new(format_node_range(active_layer.node()))) + .on_mouse_down(MouseButton::Left, move |_, cx| { + view.update(cx, |view, cx| view.toggle_menu(cx)); + }) + .cursor(CursorStyle::PointingHand) + .border_1() + .border_color(red()) } fn render_menu_item( - theme: &Arc, active_layer: &OwnedSyntaxLayerInfo, layer: SyntaxLayerInfo, layer_ix: usize, cx: &mut ViewContext, - ) -> impl Element { - enum ActivateLayer {} - MouseEventHandler::new::(layer_ix, cx, move |state, _| { - let is_selected = layer.node() == active_layer.node(); - let style = theme - .toolbar_dropdown_menu - .item - .in_state(is_selected) - .style_for(state); - Flex::row() - .with_child( - Label::new(layer.language.name().to_string(), style.text.clone()) - .contained() - .with_margin_right(style.secondary_text_spacing), - ) - .with_child(Label::new( - format_node_range(layer.node()), - style - .secondary_text - .clone() - .unwrap_or_else(|| style.text.clone()), - )) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, view, cx| { - view.select_layer(layer_ix, cx); - }) + ) -> Div { + // todo!() styling + let _is_selected = layer.node() == active_layer.node(); + let view = cx.view().clone(); + h_stack() + .child(Label::new(layer.language.name().to_string())) + .child(Label::new(format_node_range(layer.node()))) + .cursor(CursorStyle::PointingHand) + .on_mouse_down(MouseButton::Left, move |_, cx| { + view.update(cx, |view, cx| { + view.select_layer(layer_ix, cx); + }) + }) + .border_1() + .border_color(red()) } } @@ -630,33 +588,32 @@ fn format_node_range(node: Node) -> String { } impl Render for SyntaxTreeToolbarItemView { - type Element = Focusable
; + type Element = Div; // todo!() // fn ui_name() -> &'static str { // "SyntaxTreeToolbarItemView" // } - fn render(&mut self, cx: &mut ViewContext<'_, '_, Self>) -> gpui::AnyElement { - self.render_menu(cx) - .unwrap_or_else(|| Empty::new().into_any()) + fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> Div { + self.render_menu(cx).unwrap_or_else(|| div()) } } +impl EventEmitter for SyntaxTreeToolbarItemView {} + impl ToolbarItemView for SyntaxTreeToolbarItemView { fn set_active_pane_item( &mut self, active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, - ) -> workspace::ToolbarItemLocation { + ) -> ToolbarItemLocation { self.menu_open = false; 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 { - flex: Some((1., false)), - }; + return ToolbarItemLocation::PrimaryLeft; } } self.tree_view = None; From 8471a5f80b04dfcebdb701a4c1958cef63152e65 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 17:04:24 +0200 Subject: [PATCH 04/14] Finish all but the styling --- crates/gpui2/src/elements/uniform_list.rs | 8 + crates/language_tools2/src/lsp_log.rs | 170 +++++++++--------- .../language_tools2/src/syntax_tree_view.rs | 146 +++++++-------- 3 files changed, 168 insertions(+), 156 deletions(-) diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 7fba7ef47777553ae6e5e36ab8e20f5e01869360..e8587683d691143358263306403187439a52c68a 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 { diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index ca4f25e177fc3204ea155600d184d76c50bdfa08..3c3df41442e983dc5045e22c5173e0e033e22a1b 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -2,17 +2,16 @@ use collections::{HashMap, VecDeque}; use editor::{Editor, EditorElement, EditorEvent, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, div, overlay, red, AnchorCorner, AnyElement, AppContext, Context, CursorStyle, Div, - EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, - MouseButton, OverlayFitMode, ParentElement, Render, Styled, Subscription, View, ViewContext, - VisualContext, WeakModel, WindowContext, + actions, div, overlay, red, AnyElement, AppContext, Context, CursorStyle, Div, EventEmitter, + FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, MouseButton, + MouseDownEvent, 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 theme::{ActiveTheme, Theme}; -use ui::{h_stack, v_stack, Label}; +use ui::{h_stack, v_stack, Checkbox, Label}; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, @@ -83,7 +82,7 @@ actions!(debug, [OpenLanguageServerLogs]); pub fn init(cx: &mut AppContext) { let log_store = cx.build_model(|cx| LogStore::new(cx)); - cx.observe_new_views(|workspace: &mut Workspace, 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| { @@ -91,7 +90,8 @@ pub fn init(cx: &mut AppContext) { }); } - workspace.register_action(|workspace, _: &OpenLanguageServerLogs, 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( @@ -118,7 +118,7 @@ impl LogStore { if let Some(this) = this.upgrade() { this.update(&mut cx, |this, cx| { this.on_io(project, server_id, io_kind, &message, cx); - }); + })?; } } anyhow::Ok(()) @@ -130,7 +130,7 @@ impl LogStore { pub fn add_project(&mut self, project: &Model, cx: &mut ModelContext) { let weak_project = project.downgrade(); self.projects.insert( - weak_project, + project.downgrade(), ProjectState { servers: HashMap::default(), _subscriptions: [ @@ -184,7 +184,7 @@ impl LogStore { server_state._io_logs_subscription = server.as_ref().map(|server| { server.on_io(move |io_kind, message| { io_tx - .unbounded_send((weak_project, id, io_kind, message.to_string())) + .unbounded_send((weak_project.clone(), id, io_kind, message.to_string())) .ok(); }) }); @@ -197,7 +197,8 @@ impl LogStore { 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(); } } }) @@ -550,10 +551,10 @@ impl LspLogView { let language = language.await.ok(); buffer.update(&mut cx, |buffer, cx| { buffer.set_language(language, cx); - }); + }) } }) - .detach(); + .detach_and_log_err(cx); }); self.editor = editor; @@ -720,7 +721,6 @@ impl Render for LspLogToolbarItemView { // } fn render(&mut self, cx: &mut ViewContext) -> Div { - let theme = cx.theme().clone(); let Some(log_view) = self.log_view.as_ref() else { return div(); }; @@ -737,60 +737,60 @@ impl Render for LspLogToolbarItemView { None } }); - let server_selected = current_server.is_some(); + // todo!() styling + let _server_selected = current_server.is_some(); - enum LspLogScroll {} - enum Menu {} let lsp_menu = h_stack() .child(Self::render_language_server_menu_header(current_server, cx)) .children(if self.menu_open { Some( - overlay() - .child( - v_stack() - .scrollable::(0, None, cx) - .children(menu_rows.into_iter().map(|row| { - Self::render_language_server_menu_item( - row.server_id, - row.server_name, - &row.worktree_root_name, - row.rpc_trace_enabled, - row.logs_selected, - row.rpc_trace_selected, - &theme, - cx, - ) - })) - .on_down_out(MouseButton::Left, |_, this, cx| { + overlay().child( + v_stack() + // todo!() + // .scrollable::(0, None, cx) + .children(menu_rows.into_iter().map(|row| { + Self::render_language_server_menu_item( + row.server_id, + row.server_name, + &row.worktree_root_name, + row.rpc_trace_enabled, + row.logs_selected, + row.rpc_trace_selected, + cx, + ) + })) + .on_mouse_down_out(cx.listener(|this, event: &MouseDownEvent, cx| { + if event.button == MouseButton::Left { this.menu_open = false; cx.notify() - }), - ) - .with_hoverable(true) - .with_fit_mode(OverlayFitMode::SwitchAnchor) - .with_anchor_corner(AnchorCorner::TopLeft) - .with_z_index(999) - .bottom() - .left(), + } + })), + ), // todo!() + // .with_hoverable(true) + // .with_fit_mode(OverlayFitMode::SwitchAnchor) + // .with_anchor_corner(AnchorCorner::TopLeft) + // .with_z_index(999), ) } else { None }); - enum LspCleanupButton {} let log_cleanup_button = div() .child(Label::new("Clear")) - .on_mouse_down(MouseButton::Left, move |_, cx| { - if let Some(log_view) = self.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); - }); - }) - } - }) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |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); + }); + }) + } + }), + ) .cursor(CursorStyle::PointingHand); h_stack() @@ -856,8 +856,6 @@ impl LspLogToolbarItemView { current_server: Option, cx: &mut ViewContext, ) -> Div { - let view = cx.view().clone(); - enum ToggleMenu {} let label: Cow = current_server .and_then(|row| { Some( @@ -878,11 +876,12 @@ impl LspLogToolbarItemView { div() .child(Label::new(label)) .cursor(CursorStyle::PointingHand) - .on_mouse_down(MouseButton::Left, move |_, cx| { - view.update(cx, |view, cx| { + .on_mouse_down( + MouseButton::Left, + cx.listener(move |view, _, cx| { view.toggle_menu(cx); - }) - }) + }), + ) .border_1() .border_color(red()) } @@ -892,53 +891,52 @@ impl LspLogToolbarItemView { name: LanguageServerName, worktree_root_name: &str, rpc_trace_enabled: bool, - logs_selected: bool, - rpc_trace_selected: bool, - theme: &Arc, + // todo!() styling + _logs_selected: bool, + _rpc_trace_selected: bool, cx: &mut ViewContext, ) -> Div { - enum ActivateLog {} - enum ActivateRpcTrace {} - enum LanguageServerCheckbox {} - - let view = cx.view().clone(); - v_stack() .child(Label::new(format!("{} ({})", name.0, worktree_root_name))) .child( div() .child(Label::new(SERVER_LOGS)) .cursor(CursorStyle::PointingHand) - .on_mouse_down(MouseButton::Left, move |_, cx| { - view.update(cx, |view, cx| { + .on_mouse_down( + MouseButton::Left, + cx.listener(move |view, _, cx| { view.show_logs_for_server(id, cx); - }) - }), + }), + ), ) .child( h_stack() .child(Label::new(RPC_MESSAGES)) .child( - ui::checkbox_with_label::( - div(), - &theme.welcome.checkbox, - rpc_trace_enabled, + Checkbox::new( id.0, - cx, - move |this, enabled, cx| { - this.toggle_logging_for_server(id, enabled, cx); + if rpc_trace_enabled { + ui::Selection::Selected + } else { + ui::Selection::Unselected }, ) - .flex_float(), + .on_click(cx.listener( + move |this, selection, cx| { + let enabled = matches!(selection, ui::Selection::Selected); + this.toggle_logging_for_server(id, enabled, cx); + }, + )), ) .border_1() .border_color(red()) .cursor(CursorStyle::PointingHand) - .on_mouse_down(MouseButton::Left, move |_, cx| { - view.update(cx, |view, cx| { + .on_mouse_down( + MouseButton::Left, + cx.listener(move |view, _, cx| { view.show_rpc_trace_for_server(id, cx); - }) - }), + }), + ), ) .border_1() .border_color(red()) diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 7efd5f572323d08a8c2c9b9e675eeedee2fd5964..9c8ec0ee043ceb99d1569d6f67c8b829a023feef 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -2,13 +2,13 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ actions, div, overlay, red, uniform_list, AnyElement, AppContext, CursorStyle, Div, EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, ParentElement, Render, Styled, TextStyle, UniformListState, View, ViewContext, - VisualContext, WeakView, WindowContext, + MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; use settings::Settings; use std::{mem, ops::Range}; -use theme::{ActiveTheme, ThemeSettings}; +use theme::{Theme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; use ui::{h_stack, Label}; use workspace::{ @@ -35,9 +35,9 @@ pub fn init(cx: &mut AppContext) { pub struct SyntaxTreeView { workspace_handle: WeakView, editor: Option, - mouse_y: Option, - line_height: Option, - list_state: UniformListState, + mouse_y: Option, + line_height: Option, + list_scroll_handle: UniformListScrollHandle, selected_descendant_ix: Option, hovered_descendant_ix: Option, focus_handle: FocusHandle, @@ -70,7 +70,7 @@ impl SyntaxTreeView { ) -> Self { let mut this = Self { workspace_handle: workspace_handle.clone(), - list_state: UniformListState::default(), + list_scroll_handle: UniformListScrollHandle::new(), editor: None, mouse_y: None, line_height: None, @@ -204,15 +204,15 @@ impl SyntaxTreeView { let descendant_ix = cursor.descendant_index(); self.selected_descendant_ix = Some(descendant_ix); - self.list_state.scroll_to(ScrollTarget::Show(descendant_ix)); + self.list_scroll_handle.scroll_to_item(descendant_ix); cx.notify(); Some(()) } - fn handle_click(&mut self, y: f32, cx: &mut ViewContext) -> Option<()> { + fn handle_click(&mut self, y: Pixels, cx: &mut ViewContext) -> Option<()> { let line_height = self.line_height?; - let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; + 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. @@ -227,7 +227,7 @@ impl SyntaxTreeView { 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_state.scroll_top() + y) / line_height) as usize; + 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| { @@ -279,29 +279,39 @@ impl SyntaxTreeView { fn render_node( cursor: &TreeCursor, - depth: u32, + _depth: u32, selected: bool, hovered: bool, list_hovered: bool, style: &TextStyle, - editor_theme: &theme::Editor, - cx: &AppContext, + editor_theme: &Theme, + _cx: &AppContext, ) -> Div { + let editor_colors = editor_theme.colors(); let node = cursor.node(); let mut range_style = style.clone(); - let em_width = style.em_width(cx.text_system()); - let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); - - range_style.color = editor_theme.line_number; + // todo!() styling + // let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); + // let font_size = style.text.font_size.to_pixels(cx.rem_size()); + // let line_height = style.text.line_height_in_pixels(cx.rem_size()); + // let em_width = cx + // .text_system() + // .typographic_bounds(font_id, font_size, 'm') + // .unwrap() + // .size + // .width; + // let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); + + range_style.color = editor_colors.editor_line_number; let mut anonymous_node_style = style.clone(); let string_color = editor_theme - .syntax + .syntax() .highlights .iter() .find_map(|(name, style)| (name == "string").then(|| style.color)?); let property_color = editor_theme - .syntax + .syntax() .highlights .iter() .find_map(|(name, style)| (name == "property").then(|| style.color)?); @@ -330,9 +340,9 @@ impl SyntaxTreeView { ) .child(Label::new(format_node_range(node))) .text_bg(if selected { - editor_theme.selection.selection + editor_colors.element_selected } else if hovered && list_hovered { - editor_theme.active_line_background + editor_colors.element_active } else { Hsla::default() }) @@ -344,37 +354,25 @@ impl SyntaxTreeView { } impl Render for SyntaxTreeView { - // todo!() - // fn ui_name() -> &'static str { - // "SyntaxTreeView" - // } - type Element = Div; fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Div { let settings = ThemeSettings::get_global(cx); - let font = settings.buffer_font; + let font = settings.buffer_font.clone(); let font_size = settings.buffer_font_size(cx); - let editor_theme = settings.active_theme; + let editor_theme = settings.active_theme.clone(); + let editor_colors = editor_theme.colors(); let style = TextStyle { - color: editor_theme.text_color, - font_family_name, - font_family_id, - font_id, - font_size, - font_properties: Default::default(), - underline: Default::default(), - font_family: todo!(), - font_features: todo!(), - line_height: todo!(), - font_weight: todo!(), - font_style: todo!(), - background_color: todo!(), - white_space: todo!(), + color: editor_colors.text, + font_family: font.family, + font_features: font.features, + font_weight: font.weight, + font_style: font.style, + ..Default::default() }; - let line_height = cx.text_system().line_height(font_size); + let line_height = cx.text_style().line_height_in_pixels(font_size); if Some(line_height) != self.line_height { self.line_height = Some(line_height); self.hover_state_changed(cx); @@ -389,12 +387,15 @@ impl Render for SyntaxTreeView { let layer = layer.clone(); let theme = editor_theme.clone(); - let list_hovered = state.hovered(); + // todo!() + // let list_hovered = state.hovered(); + let list_hovered = false; uniform_list( - self.list_state.clone(), + cx.view().clone(), + "SyntaxTreeView", layer.node().descendant_count(), - cx, - move |this, range, items, cx| { + 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); @@ -428,17 +429,21 @@ impl Render for SyntaxTreeView { } } } + items }, ) - .on_move(move |event, this, cx| { - let y = event.position.y() - event.region.origin_y(); - this.mouse_y = Some(y); - this.hover_state_changed(cx); - }) - .on_mouse_down(MouseButton::Left, move |event, cx| { - let y = event.position.y() - event.region.origin_y(); - self.handle_click(y, cx); - }); + .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(editor_colors.background); } div() @@ -490,7 +495,6 @@ impl SyntaxTreeToolbarItemView { } fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option
{ - let theme = cx.theme().clone(); let tree_view = self.tree_view.as_ref()?; let tree_view = tree_view.read(cx); @@ -508,12 +512,12 @@ impl SyntaxTreeToolbarItemView { .children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| Self::render_menu_item(&active_layer, layer, ix, cx), )) - .on_mouse_down_out(|e, cx| { + .on_mouse_down_out(cx.listener(|this, e: &MouseDownEvent, cx| { if e.button == MouseButton::Left { - self.menu_open = false; + this.menu_open = false; cx.notify() } - }), + })), ) })), ) @@ -540,13 +544,15 @@ impl SyntaxTreeToolbarItemView { } fn render_header(active_layer: &OwnedSyntaxLayerInfo, cx: &mut ViewContext) -> Div { - let view = cx.view().clone(); h_stack() .child(Label::new(active_layer.language.name())) .child(Label::new(format_node_range(active_layer.node()))) - .on_mouse_down(MouseButton::Left, move |_, cx| { - view.update(cx, |view, cx| view.toggle_menu(cx)); - }) + .on_mouse_down( + MouseButton::Left, + cx.listener(move |view, _, cx| { + view.toggle_menu(cx); + }), + ) .cursor(CursorStyle::PointingHand) .border_1() .border_color(red()) @@ -560,16 +566,16 @@ impl SyntaxTreeToolbarItemView { ) -> Div { // todo!() styling let _is_selected = layer.node() == active_layer.node(); - let view = cx.view().clone(); h_stack() .child(Label::new(layer.language.name().to_string())) .child(Label::new(format_node_range(layer.node()))) .cursor(CursorStyle::PointingHand) - .on_mouse_down(MouseButton::Left, move |_, cx| { - view.update(cx, |view, cx| { + .on_mouse_down( + MouseButton::Left, + cx.listener(move |view, _, cx| { view.select_layer(layer_ix, cx); - }) - }) + }), + ) .border_1() .border_color(red()) } From 7e6dac383e380370d38381def811d5e47192776b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 17:14:52 +0200 Subject: [PATCH 05/14] Uncomment tests --- crates/language_tools2/src/lsp_log_tests.rs | 190 ++++++++++---------- 1 file changed, 94 insertions(+), 96 deletions(-) diff --git a/crates/language_tools2/src/lsp_log_tests.rs b/crates/language_tools2/src/lsp_log_tests.rs index 967e8a33824d75152472f3b4614b94c7a6d1671b..93e869369afa57bec6c14af6d1929b920dbf7345 100644 --- a/crates/language_tools2/src/lsp_log_tests.rs +++ b/crates/language_tools2/src/lsp_log_tests.rs @@ -1,109 +1,107 @@ -// todo!("TODO kb") -// use std::sync::Arc; +use std::sync::Arc; -// use crate::lsp_log::LogMenuItem; +use crate::lsp_log::LogMenuItem; -// use super::*; -// use futures::StreamExt; -// use gpui::{serde_json::json, TestAppContext}; -// use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; -// use project::{FakeFs, Project}; -// use settings::SettingsStore; +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(); -// } +#[gpui::test] +async fn test_lsp_logs(cx: &mut TestAppContext) { + if std::env::var("RUST_LOG").is_ok() { + env_logger::init(); + } -// init_test(cx); + 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 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()); -// 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 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.add_model(|cx| LogStore::new(cx)); -// log_store.update(cx, |store, cx| store.add_project(&project, cx)); + 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 _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 mut language_server = fake_rust_servers.next().await.unwrap(); + language_server + .receive_notification::() + .await; -// let log_view = cx -// .add_window(|cx| LspLogView::new(project.clone(), log_store.clone(), cx)) -// .root(cx); + 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.foreground().run_until_parked(); + language_server.notify::(lsp::LogMessageParams { + message: "hello from the server".into(), + typ: lsp::MessageType::INFO, + }); + cx.executor().run_until_parked(); -// log_view.read_with(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(cx) -// .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"); -// }); -// } + 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.foreground().forbid_parking(); - -// cx.update(|cx| { -// cx.set_global(SettingsStore::test(cx)); -// theme::init((), cx); -// language::init(cx); -// client::init_settings(cx); -// Project::init_settings(cx); -// editor::init_settings(cx); -// }); -// } +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); + }); +} From c1f8929e43d17f2cf595de4f7cf7e66b8c9ff6f2 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 17:28:07 +0200 Subject: [PATCH 06/14] Add new panels into zed2 --- Cargo.lock | 1 + crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 3 +-- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7237ab6905db5a5aadfa1c4613114c3f12ae6bca..3226037d51a40f06b24ed0ede79e2ee211c2ed43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12198,6 +12198,7 @@ dependencies = [ "journal2", "language2", "language_selector2", + "language_tools2", "lazy_static", "libc", "log", 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); From 3586bf8b6ba2b275ccaa2f95dca10b45a5845cfa Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 17:38:52 +0200 Subject: [PATCH 07/14] Display something --- crates/language_tools2/src/lsp_log.rs | 10 ++++++++-- crates/language_tools2/src/syntax_tree_view.rs | 18 ++++++++++++++---- crates/zed2/src/zed2.rs | 11 +++++------ 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 3c3df41442e983dc5045e22c5173e0e033e22a1b..06cdaf3bf93d5f9070dcbc600d6c78d18dca5640 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -741,11 +741,13 @@ impl Render for LspLogToolbarItemView { let _server_selected = current_server.is_some(); let lsp_menu = h_stack() + .size_full() .child(Self::render_language_server_menu_header(current_server, cx)) .children(if self.menu_open { Some( overlay().child( v_stack() + .size_full() // todo!() // .scrollable::(0, None, cx) .children(menu_rows.into_iter().map(|row| { @@ -769,11 +771,11 @@ impl Render for LspLogToolbarItemView { // .with_hoverable(true) // .with_fit_mode(OverlayFitMode::SwitchAnchor) // .with_anchor_corner(AnchorCorner::TopLeft) - // .with_z_index(999), ) } else { None - }); + }) + .z_index(99); let log_cleanup_button = div() .child(Label::new("Clear")) @@ -794,6 +796,7 @@ impl Render for LspLogToolbarItemView { .cursor(CursorStyle::PointingHand); h_stack() + .size_full() .child(lsp_menu) .child(log_cleanup_button) .border_1() @@ -897,6 +900,7 @@ impl LspLogToolbarItemView { cx: &mut ViewContext, ) -> Div { v_stack() + .size_full() .child(Label::new(format!("{} ({})", name.0, worktree_root_name))) .child( div() @@ -911,6 +915,7 @@ impl LspLogToolbarItemView { ) .child( h_stack() + .size_full() .child(Label::new(RPC_MESSAGES)) .child( Checkbox::new( @@ -940,6 +945,7 @@ impl LspLogToolbarItemView { ) .border_1() .border_color(red()) + .bg(red()) } } diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 9c8ec0ee043ceb99d1569d6f67c8b829a023feef..4c9e7529c5cde073d369aeb5716fa65e9933b117 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -319,7 +319,7 @@ impl SyntaxTreeView { anonymous_node_style.color = color; } - let mut row = h_stack(); + let mut row = h_stack().size_full(); if let Some(field_name) = cursor.field_name() { let mut field_style = style.clone(); if let Some(color) = property_color { @@ -378,6 +378,8 @@ impl Render for SyntaxTreeView { self.hover_state_changed(cx); } + let mut rendered = div(); + if let Some(layer) = self .editor .as_ref() @@ -390,7 +392,7 @@ impl Render for SyntaxTreeView { // todo!() // let list_hovered = state.hovered(); let list_hovered = false; - uniform_list( + let list = uniform_list( cx.view().clone(), "SyntaxTreeView", layer.node().descendant_count(), @@ -444,9 +446,11 @@ impl Render for SyntaxTreeView { }), ) .text_bg(editor_colors.background); + + rendered = rendered.child(list); } - div() + rendered } } @@ -505,10 +509,12 @@ impl SyntaxTreeToolbarItemView { Some( v_stack() + .size_full() .child(Self::render_header(&active_layer, cx)) .children(self.menu_open.then(|| { overlay().child( v_stack() + .size_full() .children(active_buffer.syntax_layers().enumerate().map( |(ix, layer)| Self::render_menu_item(&active_layer, layer, ix, cx), )) @@ -519,7 +525,8 @@ impl SyntaxTreeToolbarItemView { } })), ) - })), + })) + .z_index(99), ) } @@ -545,6 +552,7 @@ impl SyntaxTreeToolbarItemView { fn render_header(active_layer: &OwnedSyntaxLayerInfo, cx: &mut ViewContext) -> Div { h_stack() + .size_full() .child(Label::new(active_layer.language.name())) .child(Label::new(format_node_range(active_layer.node()))) .on_mouse_down( @@ -567,6 +575,7 @@ impl SyntaxTreeToolbarItemView { // todo!() styling let _is_selected = layer.node() == active_layer.node(); h_stack() + .size_full() .child(Label::new(layer.language.name().to_string())) .child(Label::new(format_node_range(layer.node()))) .cursor(CursorStyle::PointingHand) @@ -578,6 +587,7 @@ impl SyntaxTreeToolbarItemView { ) .border_1() .border_color(red()) + .bg(red()) } } 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); }) }); } From 6a90a76bf2353ea03a86523e287495234cc5b56b Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Fri, 15 Dec 2023 23:50:20 +0200 Subject: [PATCH 08/14] Use stock gpui2 ui components --- crates/language_tools2/src/lsp_log.rs | 183 +++++++----------- .../language_tools2/src/syntax_tree_view.rs | 143 ++++---------- crates/ui2/src/components/context_menu.rs | 2 +- 3 files changed, 114 insertions(+), 214 deletions(-) diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 06cdaf3bf93d5f9070dcbc600d6c78d18dca5640..3cff408280f0c345121822f0521424c4b5ece1cc 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -2,16 +2,18 @@ use collections::{HashMap, VecDeque}; use editor::{Editor, EditorElement, EditorEvent, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, div, overlay, red, AnyElement, AppContext, Context, CursorStyle, Div, EventEmitter, - FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, MouseButton, - MouseDownEvent, ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, + actions, div, red, AnchorCorner, AnyElement, AppContext, Context, CursorStyle, Div, + EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, + MouseButton, 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, v_stack, Checkbox, Label}; +use ui::{ + h_stack, popover_menu, v_stack, Button, Checkbox, Clickable, ContextMenu, Divider, Label, +}; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, @@ -58,7 +60,6 @@ pub struct LspLogView { pub struct LspLogToolbarItemView { log_view: Option>, _log_view_subscription: Option, - menu_open: bool, } #[derive(Copy, Clone, PartialEq, Eq)] @@ -594,11 +595,6 @@ fn log_contents(lines: &VecDeque) -> String { } impl Render for LspLogView { - // todo!() - // fn ui_name() -> &'static str { - // "LspLogView" - // } - type Element = EditorElement; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { @@ -697,7 +693,6 @@ impl ToolbarItemView for LspLogToolbarItemView { active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) -> workspace::ToolbarItemLocation { - self.menu_open = false; if let Some(item) = active_pane_item { if let Some(log_view) = item.downcast::() { self.log_view = Some(log_view.clone()); @@ -715,13 +710,9 @@ impl ToolbarItemView for LspLogToolbarItemView { impl Render for LspLogToolbarItemView { type Element = Div; - // todo!() - // fn ui_name() -> &'static str { - // "LspLogView" - // } - fn render(&mut self, cx: &mut ViewContext) -> Div { - let Some(log_view) = self.log_view.as_ref() else { + 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| { @@ -737,70 +728,63 @@ impl Render for LspLogToolbarItemView { None } }); - // todo!() styling - let _server_selected = current_server.is_some(); - let lsp_menu = h_stack() - .size_full() - .child(Self::render_language_server_menu_header(current_server, cx)) - .children(if self.menu_open { - Some( - overlay().child( - v_stack() - .size_full() - // todo!() - // .scrollable::(0, None, cx) - .children(menu_rows.into_iter().map(|row| { - Self::render_language_server_menu_item( - row.server_id, - row.server_name, - &row.worktree_root_name, - row.rpc_trace_enabled, - row.logs_selected, - row.rpc_trace_selected, - cx, - ) - })) - .on_mouse_down_out(cx.listener(|this, event: &MouseDownEvent, cx| { - if event.button == MouseButton::Left { - this.menu_open = false; - cx.notify() - } - })), - ), // todo!() - // .with_hoverable(true) - // .with_fit_mode(OverlayFitMode::SwitchAnchor) - // .with_anchor_corner(AnchorCorner::TopLeft) - ) - } else { - None - }) - .z_index(99); - - let log_cleanup_button = div() - .child(Label::new("Clear")) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |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); - }); - }) + let lsp_menu = popover_menu("LspLogView") + .anchor(AnchorCorner::TopLeft) + .trigger(Self::render_language_server_menu_header(current_server)) + .menu(move |cx| { + let menu_rows = menu_rows.clone(); + let log_view = log_view.clone(); + ContextMenu::build(cx, move |mut menu, cx| { + for row in menu_rows { + menu = menu + .header(format!( + "{} ({})", + row.server_name.0, row.worktree_root_name + )) + .entry( + format!("{SERVER_LOGS} ({})", row.server_name.0), + |cx| { + dbg!("????????????????????"); + }, // cx.handler_for(&log_view, move |view, cx| { + // // todo!() why does not it work??? + // dbg!("~~~~~~~~~~~~~~~~~~~~~~~~~~??@@@#", row.server_id); + // view.show_logs_for_server(row.server_id, cx) + // }), + ) + // TODO kb custom element with checkbox & toggle logging for server + .entry( + format!("{RPC_MESSAGES} ({})", row.server_name.0), + |cx| { + dbg!("?????????????@@@@@@@@@@@@@@@"); + }, // cx.handler_for(&log_view, move |view, cx| { + // view.show_rpc_trace_for_server(row.server_id, cx) + // }), + ) } - }), - ) - .cursor(CursorStyle::PointingHand); + menu + }) + }); - h_stack() - .size_full() - .child(lsp_menu) - .child(log_cleanup_button) - .border_1() - .border_color(red()) + 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(), + ) } } @@ -810,17 +794,11 @@ const SERVER_LOGS: &str = "Server Logs"; impl LspLogToolbarItemView { pub fn new() -> Self { Self { - menu_open: false, log_view: None, _log_view_subscription: None, } } - fn toggle_menu(&mut self, cx: &mut ViewContext) { - self.menu_open = !self.menu_open; - cx.notify(); - } - fn toggle_logging_for_server( &mut self, id: LanguageServerId, @@ -842,7 +820,6 @@ impl LspLogToolbarItemView { fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { if let Some(log_view) = &self.log_view { log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx)); - self.menu_open = false; cx.notify(); } } @@ -850,19 +827,16 @@ impl LspLogToolbarItemView { fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { if let Some(log_view) = &self.log_view { log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx)); - self.menu_open = false; cx.notify(); } } - fn render_language_server_menu_header( - current_server: Option, - cx: &mut ViewContext, - ) -> Div { - let label: Cow = current_server - .and_then(|row| { - Some( - format!( + fn render_language_server_menu_header(current_server: Option) -> Button { + Button::new( + "language_server_menu_header", + current_server + .and_then(|row| { + Some(Cow::Owned(format!( "{} ({}) - {}", row.server_name.0, row.worktree_root_name, @@ -871,22 +845,10 @@ impl LspLogToolbarItemView { } else { SERVER_LOGS }, - ) - .into(), - ) - }) - .unwrap_or_else(|| "No server selected".into()); - div() - .child(Label::new(label)) - .cursor(CursorStyle::PointingHand) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |view, _, cx| { - view.toggle_menu(cx); - }), - ) - .border_1() - .border_color(red()) + ))) + }) + .unwrap_or_else(|| "No server selected".into()), + ) } fn render_language_server_menu_item( @@ -894,7 +856,6 @@ impl LspLogToolbarItemView { name: LanguageServerName, worktree_root_name: &str, rpc_trace_enabled: bool, - // todo!() styling _logs_selected: bool, _rpc_trace_selected: bool, cx: &mut ViewContext, diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 4c9e7529c5cde073d369aeb5716fa65e9933b117..9b205cb28bddeaf2af016329fea045e206f0c65d 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -1,20 +1,19 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ - actions, div, overlay, red, uniform_list, AnyElement, AppContext, CursorStyle, Div, - EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, - MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, TextStyle, - UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, + actions, div, rems, uniform_list, AnyElement, AppContext, Div, EventEmitter, FocusHandle, + FocusableView, Hsla, InteractiveElement, IntoElement, Model, MouseButton, MouseDownEvent, + MouseMoveEvent, ParentElement, Pixels, Render, Styled, TextStyle, UniformListScrollHandle, + View, ViewContext, VisualContext, WeakView, WindowContext, }; -use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +use language::{Buffer, OwnedSyntaxLayerInfo}; use settings::Settings; use std::{mem, ops::Range}; use theme::{Theme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; -use ui::{h_stack, Label}; +use ui::{h_stack, popover_menu, ButtonLike, ContextMenu, Label, PopoverMenu}; use workspace::{ item::{Item, ItemHandle}, - ui::v_stack, - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, + SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(debug, [OpenSyntaxTreeView]); @@ -26,7 +25,7 @@ pub fn init(cx: &mut AppContext) { let workspace_handle = workspace.weak_handle(); let syntax_tree_view = cx.build_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); - workspace.add_item(Box::new(syntax_tree_view), cx); + workspace.split_item(SplitDirection::Right, Box::new(syntax_tree_view), cx) }); }) .detach(); @@ -46,7 +45,6 @@ pub struct SyntaxTreeView { pub struct SyntaxTreeToolbarItemView { tree_view: Option>, subscription: Option, - menu_open: bool, } struct EditorState { @@ -279,7 +277,7 @@ impl SyntaxTreeView { fn render_node( cursor: &TreeCursor, - _depth: u32, + depth: u32, selected: bool, hovered: bool, list_hovered: bool, @@ -290,18 +288,6 @@ impl SyntaxTreeView { let editor_colors = editor_theme.colors(); let node = cursor.node(); let mut range_style = style.clone(); - // todo!() styling - // let font_id = cx.text_system().font_id(&style.text.font()).unwrap(); - // let font_size = style.text.font_size.to_pixels(cx.rem_size()); - // let line_height = style.text.line_height_in_pixels(cx.rem_size()); - // let em_width = cx - // .text_system() - // .typographic_bounds(font_id, font_size, 'm') - // .unwrap() - // .size - // .width; - // let gutter_padding = (em_width * editor_theme.gutter_padding_factor).round(); - range_style.color = editor_colors.editor_line_number; let mut anonymous_node_style = style.clone(); @@ -335,8 +321,7 @@ impl SyntaxTreeView { Label::new(node.kind()) } else { Label::new(format!("\"{}\"", node.kind())) - }, // todo!() - // .margin(em_width), + }, ) .child(Label::new(format_node_range(node))) .text_bg(if selected { @@ -346,10 +331,10 @@ impl SyntaxTreeView { } else { Hsla::default() }) - // todo!() + // todo!() does not work + .ml(rems(depth as f32 * 180.0)) // .padding(gutter_padding + depth as f32 * 18.0) - .border_1() - .border_color(red()); + ; } } @@ -389,8 +374,6 @@ impl Render for SyntaxTreeView { let layer = layer.clone(); let theme = editor_theme.clone(); - // todo!() - // let list_hovered = state.hovered(); let list_hovered = false; let list = uniform_list( cx.view().clone(), @@ -434,6 +417,7 @@ impl Render for SyntaxTreeView { items }, ) + // todo!() does scroll either editor or the tree .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); @@ -492,13 +476,12 @@ impl Item for SyntaxTreeView { impl SyntaxTreeToolbarItemView { pub fn new() -> Self { Self { - menu_open: false, tree_view: None, subscription: None, } } - fn render_menu(&mut self, cx: &mut ViewContext<'_, Self>) -> Option
{ + 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); @@ -507,34 +490,30 @@ impl SyntaxTreeToolbarItemView { let active_layer = buffer_state.active_layer.clone()?; let active_buffer = buffer_state.buffer.read(cx).snapshot(); + let view = cx.view().clone(); Some( - v_stack() - .size_full() - .child(Self::render_header(&active_layer, cx)) - .children(self.menu_open.then(|| { - overlay().child( - v_stack() - .size_full() - .children(active_buffer.syntax_layers().enumerate().map( - |(ix, layer)| Self::render_menu_item(&active_layer, layer, ix, cx), - )) - .on_mouse_down_out(cx.listener(|this, e: &MouseDownEvent, cx| { - if e.button == MouseButton::Left { - this.menu_open = false; - cx.notify() - } - })), - ) - })) - .z_index(99), + 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 toggle_menu(&mut self, cx: &mut ViewContext) { - self.menu_open = !self.menu_open; - cx.notify(); - } - 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| { @@ -544,50 +523,15 @@ impl SyntaxTreeToolbarItemView { let layer = snapshot.syntax_layers().nth(layer_ix)?; buffer_state.active_layer = Some(layer.to_owned()); view.selected_descendant_ix = None; - self.menu_open = false; cx.notify(); Some(()) }) } - fn render_header(active_layer: &OwnedSyntaxLayerInfo, cx: &mut ViewContext) -> Div { - h_stack() - .size_full() + 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()))) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |view, _, cx| { - view.toggle_menu(cx); - }), - ) - .cursor(CursorStyle::PointingHand) - .border_1() - .border_color(red()) - } - - fn render_menu_item( - active_layer: &OwnedSyntaxLayerInfo, - layer: SyntaxLayerInfo, - layer_ix: usize, - cx: &mut ViewContext, - ) -> Div { - // todo!() styling - let _is_selected = layer.node() == active_layer.node(); - h_stack() - .size_full() - .child(Label::new(layer.language.name().to_string())) - .child(Label::new(format_node_range(layer.node()))) - .cursor(CursorStyle::PointingHand) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |view, _, cx| { - view.select_layer(layer_ix, cx); - }), - ) - .border_1() - .border_color(red()) - .bg(red()) } } @@ -604,15 +548,11 @@ fn format_node_range(node: Node) -> String { } impl Render for SyntaxTreeToolbarItemView { - type Element = Div; - - // todo!() - // fn ui_name() -> &'static str { - // "SyntaxTreeToolbarItemView" - // } + type Element = PopoverMenu; - fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> Div { - self.render_menu(cx).unwrap_or_else(|| div()) + fn render(&mut self, cx: &mut ViewContext<'_, Self>) -> PopoverMenu { + self.render_menu(cx) + .unwrap_or_else(|| popover_menu("Empty Syntax Tree")) } } @@ -624,7 +564,6 @@ impl ToolbarItemView for SyntaxTreeToolbarItemView { active_pane_item: Option<&dyn ItemHandle>, cx: &mut ViewContext, ) -> ToolbarItemLocation { - self.menu_open = false; if let Some(item) = active_pane_item { if let Some(view) = item.downcast::() { self.tree_view = Some(view.clone()); diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 8fce15d1c69d0fcf97f8ac1757874be4884950b6..4e3c03ad80a1ab1f66b84d69685a0d5260401fda 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -9,7 +9,7 @@ use gpui::{ use menu::{SelectFirst, SelectLast, SelectNext, SelectPrev}; use std::{rc::Rc, time::Duration}; -pub enum ContextMenuItem { +enum ContextMenuItem { Separator, Header(SharedString), Entry { From 7b83d65debcba1c1b4967be88843b98a9f8f200d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Mon, 18 Dec 2023 19:15:54 +0200 Subject: [PATCH 09/14] Remove temporary imports, fix syntax_tree_view --- crates/gpui2/src/elements/div.rs | 39 ++-- crates/gpui2/src/elements/uniform_list.rs | 5 + crates/language_tools2/src/lsp_log.rs | 190 +++++++----------- .../language_tools2/src/syntax_tree_view.rs | 30 ++- 4 files changed, 124 insertions(+), 140 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 908b302dfc1cd28f6ff873eff215d583d1b489f4..24784d695c69983e69b94726a514f6890e5f5a30 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1427,8 +1427,16 @@ impl Interactivity { let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); let interactive_bounds = interactive_bounds.clone(); - + let id = self.element_id.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { + if id == Some(ElementId::Name("SyntaxTreeView".into())) { + dbg!( + &overflow, + event.position, + &interactive_bounds, + interactive_bounds.visibly_contains(&event.position, cx) + ); + } if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, cx) { @@ -1442,9 +1450,15 @@ impl Interactivity { } if overflow.y == Overflow::Scroll { - scroll_offset.y = - (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); + if id == Some(ElementId::Name("SyntaxTreeView".into())) { + println!("prev scroll offset: {old_scroll_offset:?}, scroll_max: {scroll_max:?}, delta:{delta:?}"); + } + scroll_offset.y = + (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); + if id == Some(ElementId::Name("SyntaxTreeView".into())) { + println!("new scroll offset: {scroll_offset:?}, scroll_max: {scroll_max:?}, delta:{delta:?}"); } + } if *scroll_offset != old_scroll_offset { cx.notify(); @@ -1487,15 +1501,16 @@ impl Interactivity { } 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) - }) - }, - ); + 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) + }) + }, + ); if let Some(group) = self.group.as_ref() { GroupBounds::pop(group, cx); diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index e8587683d691143358263306403187439a52c68a..016f0160247e5931580a41d35f5052cf1f7df30d 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -127,6 +127,7 @@ impl Element for UniformList { .map(|s| s.item_size) .unwrap_or_else(|| self.measure_item(None, cx)); + let element_id = self.interactivity.element_id.clone(); let (layout_id, interactive) = self.interactivity .layout(state.map(|s| s.interactive), cx, |style, cx| { @@ -143,6 +144,10 @@ impl Element for UniformList { item_size.width } }); + + if element_id == Some(ElementId::Name("SyntaxTreeView".into())) { + dbg!(known_dimensions, available_space.height); + } let height = match available_space.height { AvailableSpace::Definite(height) => desired_height.min(height), AvailableSpace::MinContent | AvailableSpace::MaxContent => { diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 3cff408280f0c345121822f0521424c4b5ece1cc..9a8ec9f71aef31430187e6f8e320373eef6aa337 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -2,18 +2,16 @@ use collections::{HashMap, VecDeque}; use editor::{Editor, EditorElement, EditorEvent, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ - actions, div, red, AnchorCorner, AnyElement, AppContext, Context, CursorStyle, Div, - EventEmitter, FocusHandle, FocusableView, InteractiveElement, IntoElement, Model, ModelContext, - MouseButton, ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, - WeakModel, WindowContext, + actions, div, AnchorCorner, AnyElement, AppContext, Context, Div, EventEmitter, FocusHandle, + FocusableView, InteractiveElement, IntoElement, Model, ModelContext, MouseButton, + 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, v_stack, Button, Checkbox, Clickable, ContextMenu, Divider, Label, -}; +use ui::{h_stack, popover_menu, Button, Checkbox, Clickable, ContextMenu, Label, Selection}; use workspace::{ item::{Item, ItemHandle}, searchable::{SearchEvent, SearchableItem, SearchableItemHandle}, @@ -729,38 +727,86 @@ impl Render for LspLogToolbarItemView { } }); + let log_toolbar_view = cx.view().clone(); let lsp_menu = popover_menu("LspLogView") .anchor(AnchorCorner::TopLeft) - .trigger(Self::render_language_server_menu_header(current_server)) + .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 row in menu_rows { + for (ix, row) in menu_rows.into_iter().enumerate() { menu = menu .header(format!( "{} ({})", row.server_name.0, row.worktree_root_name )) .entry( - format!("{SERVER_LOGS} ({})", row.server_name.0), - |cx| { - dbg!("????????????????????"); - }, // cx.handler_for(&log_view, move |view, cx| { - // // todo!() why does not it work??? - // dbg!("~~~~~~~~~~~~~~~~~~~~~~~~~~??@@@#", row.server_id); - // view.show_logs_for_server(row.server_id, cx) - // }), - ) - // TODO kb custom element with checkbox & toggle logging for server - .entry( - format!("{RPC_MESSAGES} ({})", row.server_name.0), - |cx| { - dbg!("?????????????@@@@@@@@@@@@@@@"); - }, // cx.handler_for(&log_view, move |view, cx| { - // view.show_rpc_trace_for_server(row.server_id, cx) - // }), + SERVER_LOGS, + cx.handler_for(&log_view, move |view, cx| { + view.show_logs_for_server(row.server_id, cx) + }), ) + .custom_entry({ + let log_view = log_view.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + move |cx| { + h_stack() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .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, + ); + }, + ), + ), + ) + .on_mouse_down( + MouseButton::Left, + cx.listener_for(&log_view, move |view, _, cx| { + view.show_rpc_trace_for_server(row.server_id, cx) + }), + ) + .into_any_element() + } + }) } menu }) @@ -816,98 +862,6 @@ impl LspLogToolbarItemView { } cx.notify(); } - - fn show_logs_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { - if let Some(log_view) = &self.log_view { - log_view.update(cx, |view, cx| view.show_logs_for_server(id, cx)); - cx.notify(); - } - } - - fn show_rpc_trace_for_server(&mut self, id: LanguageServerId, cx: &mut ViewContext) { - if let Some(log_view) = &self.log_view { - log_view.update(cx, |view, cx| view.show_rpc_trace_for_server(id, cx)); - cx.notify(); - } - } - - fn render_language_server_menu_header(current_server: Option) -> Button { - 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()), - ) - } - - fn render_language_server_menu_item( - id: LanguageServerId, - name: LanguageServerName, - worktree_root_name: &str, - rpc_trace_enabled: bool, - _logs_selected: bool, - _rpc_trace_selected: bool, - cx: &mut ViewContext, - ) -> Div { - v_stack() - .size_full() - .child(Label::new(format!("{} ({})", name.0, worktree_root_name))) - .child( - div() - .child(Label::new(SERVER_LOGS)) - .cursor(CursorStyle::PointingHand) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |view, _, cx| { - view.show_logs_for_server(id, cx); - }), - ), - ) - .child( - h_stack() - .size_full() - .child(Label::new(RPC_MESSAGES)) - .child( - Checkbox::new( - id.0, - if rpc_trace_enabled { - ui::Selection::Selected - } else { - ui::Selection::Unselected - }, - ) - .on_click(cx.listener( - move |this, selection, cx| { - let enabled = matches!(selection, ui::Selection::Selected); - this.toggle_logging_for_server(id, enabled, cx); - }, - )), - ) - .border_1() - .border_color(red()) - .cursor(CursorStyle::PointingHand) - .on_mouse_down( - MouseButton::Left, - cx.listener(move |view, _, cx| { - view.show_rpc_trace_for_server(id, cx); - }), - ), - ) - .border_1() - .border_color(red()) - .bg(red()) - } } pub enum Event { diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 9b205cb28bddeaf2af016329fea045e206f0c65d..62efda19b50ad7c0861f5971cd6b30ee8f23c4bc 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -1,9 +1,9 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ - actions, div, rems, uniform_list, AnyElement, AppContext, Div, EventEmitter, FocusHandle, - FocusableView, Hsla, InteractiveElement, IntoElement, Model, MouseButton, MouseDownEvent, - MouseMoveEvent, ParentElement, Pixels, Render, Styled, TextStyle, UniformListScrollHandle, - View, ViewContext, VisualContext, WeakView, WindowContext, + actions, canvas, div, rems, uniform_list, AnyElement, AppContext, AvailableSpace, Div, + EventEmitter, FocusHandle, FocusableView, Hsla, InteractiveElement, IntoElement, Model, + MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{Buffer, OwnedSyntaxLayerInfo}; use settings::Settings; @@ -305,7 +305,7 @@ impl SyntaxTreeView { anonymous_node_style.color = color; } - let mut row = h_stack().size_full(); + let mut row = h_stack().bg(gpui::yellow()); if let Some(field_name) = cursor.field_name() { let mut field_style = style.clone(); if let Some(color) = property_color { @@ -332,7 +332,8 @@ impl SyntaxTreeView { Hsla::default() }) // todo!() does not work - .ml(rems(depth as f32 * 180.0)) + // .ml(rems(dbg!(depth) as f32 * 10.0)) + .pl(rems(dbg!(depth) as f32 * 10.0)) // .padding(gutter_padding + depth as f32 * 18.0) ; } @@ -341,7 +342,7 @@ impl SyntaxTreeView { impl Render for SyntaxTreeView { type Element = Div; - fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Div { + fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Self::Element { let settings = ThemeSettings::get_global(cx); let font = settings.buffer_font.clone(); let font_size = settings.buffer_font_size(cx); @@ -363,7 +364,7 @@ impl Render for SyntaxTreeView { self.hover_state_changed(cx); } - let mut rendered = div(); + let mut rendered = div().flex_1(); if let Some(layer) = self .editor @@ -417,7 +418,7 @@ impl Render for SyntaxTreeView { items }, ) - // todo!() does scroll either editor or the tree + .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); @@ -431,7 +432,16 @@ impl Render for SyntaxTreeView { ) .text_bg(editor_colors.background); - rendered = rendered.child(list); + rendered = rendered.child( + canvas(move |bounds, cx| { + list.into_any_element().draw( + bounds.origin, + bounds.size.map(AvailableSpace::Definite), + cx, + ) + }) + .size_full(), + ); } rendered From 1474816a932bbb50018591059573e182847a4d5a Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 20 Dec 2023 12:18:26 +0200 Subject: [PATCH 10/14] Syntax tree panel style fixes --- crates/gpui2/src/elements/div.rs | 37 +++------ .../language_tools2/src/syntax_tree_view.rs | 80 +++++-------------- crates/ui2/src/components/context_menu.rs | 36 ++++++--- 3 files changed, 56 insertions(+), 97 deletions(-) diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index 24784d695c69983e69b94726a514f6890e5f5a30..55791a56a9306a721551ad9755a39c5eb959b293 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -1427,16 +1427,7 @@ impl Interactivity { let line_height = cx.line_height(); let scroll_max = (content_size - bounds.size).max(&Size::default()); let interactive_bounds = interactive_bounds.clone(); - let id = self.element_id.clone(); cx.on_mouse_event(move |event: &ScrollWheelEvent, phase, cx| { - if id == Some(ElementId::Name("SyntaxTreeView".into())) { - dbg!( - &overflow, - event.position, - &interactive_bounds, - interactive_bounds.visibly_contains(&event.position, cx) - ); - } if phase == DispatchPhase::Bubble && interactive_bounds.visibly_contains(&event.position, cx) { @@ -1450,15 +1441,9 @@ impl Interactivity { } if overflow.y == Overflow::Scroll { - if id == Some(ElementId::Name("SyntaxTreeView".into())) { - println!("prev scroll offset: {old_scroll_offset:?}, scroll_max: {scroll_max:?}, delta:{delta:?}"); - } - scroll_offset.y = - (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); - if id == Some(ElementId::Name("SyntaxTreeView".into())) { - println!("new scroll offset: {scroll_offset:?}, scroll_max: {scroll_max:?}, delta:{delta:?}"); + scroll_offset.y = + (scroll_offset.y + delta.y).clamp(-scroll_max.height, px(0.)); } - } if *scroll_offset != old_scroll_offset { cx.notify(); @@ -1501,16 +1486,16 @@ impl Interactivity { } 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) - } + 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) - }) - }, - ); + f(style, scroll_offset.unwrap_or_default(), cx) + }) + }, + ); if let Some(group) = self.group.as_ref() { GroupBounds::pop(group, cx); diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 62efda19b50ad7c0861f5971cd6b30ee8f23c4bc..0fd33cb3cdd9e32976cf626d4f399632fbd1d6e8 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -2,7 +2,7 @@ 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, TextStyle, + MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, Pixels, Render, Styled, UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use language::{Buffer, OwnedSyntaxLayerInfo}; @@ -10,7 +10,7 @@ use settings::Settings; use std::{mem, ops::Range}; use theme::{Theme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; -use ui::{h_stack, popover_menu, ButtonLike, ContextMenu, Label, PopoverMenu}; +use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; use workspace::{ item::{Item, ItemHandle}, SplitDirection, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -279,63 +279,34 @@ impl SyntaxTreeView { cursor: &TreeCursor, depth: u32, selected: bool, - hovered: bool, - list_hovered: bool, - style: &TextStyle, editor_theme: &Theme, _cx: &AppContext, ) -> Div { let editor_colors = editor_theme.colors(); - let node = cursor.node(); - let mut range_style = style.clone(); - range_style.color = editor_colors.editor_line_number; - - let mut anonymous_node_style = style.clone(); - let string_color = editor_theme - .syntax() - .highlights - .iter() - .find_map(|(name, style)| (name == "string").then(|| style.color)?); - let property_color = editor_theme - .syntax() - .highlights - .iter() - .find_map(|(name, style)| (name == "property").then(|| style.color)?); - if let Some(color) = string_color { - anonymous_node_style.color = color; - } - - let mut row = h_stack().bg(gpui::yellow()); + let mut row = h_stack(); if let Some(field_name) = cursor.field_name() { - let mut field_style = style.clone(); - if let Some(color) = property_color { - field_style.color = color; - } - - row = row.children([Label::new(field_name), Label::new(": ")]); + 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( - if node.is_named() { - Label::new(node.kind()) - } else { - Label::new(format!("\"{}\"", node.kind())) - }, + div() + .child(Label::new(format_node_range(node)).color(Color::Muted)) + .pl_1(), ) - .child(Label::new(format_node_range(node))) .text_bg(if selected { editor_colors.element_selected - } else if hovered && list_hovered { - editor_colors.element_active } else { Hsla::default() }) - // todo!() does not work - // .ml(rems(dbg!(depth) as f32 * 10.0)) - .pl(rems(dbg!(depth) as f32 * 10.0)) - // .padding(gutter_padding + depth as f32 * 18.0) - ; + .pl(rems(depth as f32)) + .hover(|style| style.bg(editor_colors.element_active)); } } @@ -344,21 +315,11 @@ impl Render for SyntaxTreeView { fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Self::Element { let settings = ThemeSettings::get_global(cx); - let font = settings.buffer_font.clone(); - let font_size = settings.buffer_font_size(cx); - let editor_theme = settings.active_theme.clone(); let editor_colors = editor_theme.colors(); - let style = TextStyle { - color: editor_colors.text, - font_family: font.family, - font_features: font.features, - font_weight: font.weight, - font_style: font.style, - ..Default::default() - }; - - let line_height = cx.text_style().line_height_in_pixels(font_size); + 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); @@ -374,8 +335,6 @@ impl Render for SyntaxTreeView { { let layer = layer.clone(); let theme = editor_theme.clone(); - - let list_hovered = false; let list = uniform_list( cx.view().clone(), "SyntaxTreeView", @@ -401,9 +360,6 @@ impl Render for SyntaxTreeView { &cursor, depth, Some(descendant_ix) == this.selected_descendant_ix, - Some(descendant_ix) == this.hovered_descendant_ix, - list_hovered, - &style, &theme, cx, )); diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 4e3c03ad80a1ab1f66b84d69685a0d5260401fda..940d41a94c8976561c145ffe5aced81f5909b377 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -3,8 +3,8 @@ 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}; @@ -18,6 +18,9 @@ enum ContextMenuItem { handler: Rc, action: Option>, }, + CustomEntry { + entry_render: Box AnyElement>, + }, } pub struct ContextMenu { @@ -83,6 +86,16 @@ impl ContextMenu { self } + pub fn custom_entry( + mut self, + entry_render: impl Fn(&mut WindowContext) -> AnyElement + 'static, + ) -> Self { + self.items.push(ContextMenuItem::CustomEntry { + entry_render: Box::new(entry_render), + }); + self + } + pub fn action(mut self, label: impl Into, action: Box) -> Self { self.items.push(ContextMenuItem::Entry { label: label.into(), @@ -230,9 +243,9 @@ impl Render for ContextMenu { el }) .flex_none() - .child( - List::new().children(self.items.iter().enumerate().map( - |(ix, item)| match item { + .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() @@ -255,7 +268,7 @@ impl Render for ContextMenu { Label::new(label.clone()).into_any_element() }; - ListItem::new(label.clone()) + ListItem::new(ix) .inset(true) .selected(Some(ix) == self.selected_index) .on_click(move |_, cx| handler(cx)) @@ -271,9 +284,14 @@ impl Render for ContextMenu { ) .into_any_element() } - }, - )), - ), + ContextMenuItem::CustomEntry { entry_render } => ListItem::new(ix) + .inset(true) + .selected(Some(ix) == self.selected_index) + .child(entry_render(cx)) + .into_any_element(), + } + }, + ))), ) } } From fe40e3920bc1126bc5ad2186e09c0b407db6a20d Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 20 Dec 2023 17:02:03 +0200 Subject: [PATCH 11/14] Fix remaining z-index bugs Co-Authored-By: Antonio Scandurra --- crates/collab_ui2/src/collab_titlebar_item.rs | 1 - crates/gpui2/src/window.rs | 10 ++++++++++ crates/ui2/src/components/tab_bar.rs | 1 - crates/workspace2/src/toolbar.rs | 1 - 4 files changed, 10 insertions(+), 3 deletions(-) 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/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/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) From 19d15681406ad2cbf7db855a42320c75eecfd5e9 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 20 Dec 2023 17:11:04 +0200 Subject: [PATCH 12/14] Lsp log selector fixes --- crates/gpui2/src/elements/uniform_list.rs | 4 - crates/language_tools2/src/lsp_log.rs | 104 ++++++++++-------- .../language_tools2/src/syntax_tree_view.rs | 22 +--- crates/ui2/src/components/context_menu.rs | 23 ++-- 4 files changed, 82 insertions(+), 71 deletions(-) diff --git a/crates/gpui2/src/elements/uniform_list.rs b/crates/gpui2/src/elements/uniform_list.rs index 016f0160247e5931580a41d35f5052cf1f7df30d..82eac590977f7c01e53e33a5ca257a9c1a790030 100644 --- a/crates/gpui2/src/elements/uniform_list.rs +++ b/crates/gpui2/src/elements/uniform_list.rs @@ -127,7 +127,6 @@ impl Element for UniformList { .map(|s| s.item_size) .unwrap_or_else(|| self.measure_item(None, cx)); - let element_id = self.interactivity.element_id.clone(); let (layout_id, interactive) = self.interactivity .layout(state.map(|s| s.interactive), cx, |style, cx| { @@ -145,9 +144,6 @@ impl Element for UniformList { } }); - if element_id == Some(ElementId::Name("SyntaxTreeView".into())) { - dbg!(known_dimensions, available_space.height); - } let height = match available_space.height { AvailableSpace::Definite(height) => desired_height.min(height), AvailableSpace::MinContent | AvailableSpace::MaxContent => { diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 9a8ec9f71aef31430187e6f8e320373eef6aa337..20039d3908c7d8185df92441ea295a134acc78f4 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -520,6 +520,7 @@ impl LspLogView { self.editor_subscription = editor_subscription; cx.notify(); } + cx.focus(&self.focus_handle); } fn show_rpc_trace_for_server( @@ -560,6 +561,8 @@ impl LspLogView { self.editor_subscription = editor_subscription; cx.notify(); } + + cx.focus(&self.focus_handle); } fn toggle_rpc_trace_for_server( @@ -753,6 +756,7 @@ impl Render for LspLogToolbarItemView { 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!( "{} ({})", @@ -761,52 +765,65 @@ impl Render for LspLogToolbarItemView { .entry( SERVER_LOGS, cx.handler_for(&log_view, move |view, cx| { - view.show_logs_for_server(row.server_id, cx) + view.show_logs_for_server(row.server_id, cx); }), - ) - .custom_entry({ - let log_view = log_view.clone(); - let log_toolbar_view = log_toolbar_view.clone(); - move |cx| { - h_stack() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .child( - Checkbox::new( - ix, - if row.rpc_trace_enabled { - Selection::Selected - } else { - Selection::Unselected + ); + 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_view = log_view.clone(); + let log_toolbar_view = log_toolbar_view.clone(); + move |cx| { + h_stack() + .w_full() + .justify_between() + .child(Label::new(RPC_MESSAGES)) + .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, + ); }, - ) - .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, - ); - }, - ), ), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener_for(&log_view, move |view, _, cx| { - view.show_rpc_trace_for_server(row.server_id, cx) - }), - ) - .into_any_element() - } - }) + ), + ) + .on_mouse_down( + MouseButton::Left, + cx.listener_for(&log_view, move |view, _, cx| { + view.show_rpc_trace_for_server(row.server_id, cx); + }), + ) + .into_any_element() + } + }); + 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 }) @@ -858,6 +875,7 @@ impl LspLogToolbarItemView { log_view.show_logs_for_server(id, cx); cx.notify(); } + cx.focus(&log_view.focus_handle); }); } cx.notify(); diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index 0fd33cb3cdd9e32976cf626d4f399632fbd1d6e8..f0b03422cb25251faf6d5e0d6bfdcecb6aa810cd 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -8,7 +8,7 @@ use gpui::{ use language::{Buffer, OwnedSyntaxLayerInfo}; use settings::Settings; use std::{mem, ops::Range}; -use theme::{Theme, ThemeSettings}; +use theme::{ActiveTheme, ThemeSettings}; use tree_sitter::{Node, TreeCursor}; use ui::{h_stack, popover_menu, ButtonLike, Color, ContextMenu, Label, LabelCommon, PopoverMenu}; use workspace::{ @@ -275,14 +275,8 @@ impl SyntaxTreeView { Some(()) } - fn render_node( - cursor: &TreeCursor, - depth: u32, - selected: bool, - editor_theme: &Theme, - _cx: &AppContext, - ) -> Div { - let editor_colors = editor_theme.colors(); + 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(": ")]); @@ -301,12 +295,12 @@ impl SyntaxTreeView { .pl_1(), ) .text_bg(if selected { - editor_colors.element_selected + colors.element_selected } else { Hsla::default() }) .pl(rems(depth as f32)) - .hover(|style| style.bg(editor_colors.element_active)); + .hover(|style| style.bg(colors.element_hover)); } } @@ -315,8 +309,6 @@ impl Render for SyntaxTreeView { fn render(&mut self, cx: &mut gpui::ViewContext<'_, Self>) -> Self::Element { let settings = ThemeSettings::get_global(cx); - let editor_theme = settings.active_theme.clone(); - let editor_colors = editor_theme.colors(); let line_height = cx .text_style() .line_height_in_pixels(settings.buffer_font_size(cx)); @@ -334,7 +326,6 @@ impl Render for SyntaxTreeView { .and_then(|buffer| buffer.active_layer.as_ref()) { let layer = layer.clone(); - let theme = editor_theme.clone(); let list = uniform_list( cx.view().clone(), "SyntaxTreeView", @@ -360,7 +351,6 @@ impl Render for SyntaxTreeView { &cursor, depth, Some(descendant_ix) == this.selected_descendant_ix, - &theme, cx, )); descendant_ix += 1; @@ -386,7 +376,7 @@ impl Render for SyntaxTreeView { tree_view.handle_click(event.position.y, cx); }), ) - .text_bg(editor_colors.background); + .text_bg(cx.theme().colors().background); rendered = rendered.child( canvas(move |bounds, cx| { diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 940d41a94c8976561c145ffe5aced81f5909b377..1fda44e86d7f7529de4c3b014d12ce5982dd9e26 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -122,6 +122,7 @@ impl ContextMenu { { (handler)(cx) } + cx.emit(DismissEvent); } @@ -135,14 +136,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) { @@ -169,7 +176,7 @@ impl ContextMenu { } } } else { - self.select_last(&Default::default(), cx); + self.handle_select_last(&Default::default(), cx); } } @@ -195,7 +202,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); @@ -207,7 +214,7 @@ impl ContextMenu { impl ContextMenuItem { fn is_selectable(&self) -> bool { - matches!(self, Self::Entry { .. }) + matches!(self, Self::Entry { .. } | Self::CustomEntry { .. }) } } @@ -219,10 +226,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)) From 331af82cb16dbb474cc92fff4d57cb867ee916bf Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 20 Dec 2023 18:22:39 +0200 Subject: [PATCH 13/14] Rework context menu's custom element API to handle clicks properly --- crates/language_tools2/src/lsp_log.rs | 86 ++++++++++---------- crates/ui2/src/components/context_menu.rs | 98 +++++++++++++---------- 2 files changed, 97 insertions(+), 87 deletions(-) diff --git a/crates/language_tools2/src/lsp_log.rs b/crates/language_tools2/src/lsp_log.rs index 20039d3908c7d8185df92441ea295a134acc78f4..597c9c5b543fb5976c8019981216396b83739d9d 100644 --- a/crates/language_tools2/src/lsp_log.rs +++ b/crates/language_tools2/src/lsp_log.rs @@ -3,9 +3,8 @@ use editor::{Editor, EditorElement, EditorEvent, MoveToEnd}; use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, div, AnchorCorner, AnyElement, AppContext, Context, Div, EventEmitter, FocusHandle, - FocusableView, InteractiveElement, IntoElement, Model, ModelContext, MouseButton, - ParentElement, Render, Styled, Subscription, View, ViewContext, VisualContext, WeakModel, - WindowContext, + FocusableView, IntoElement, Model, ModelContext, ParentElement, Render, Styled, Subscription, + View, ViewContext, VisualContext, WeakModel, WindowContext, }; use language::{LanguageServerId, LanguageServerName}; use lsp::IoKind; @@ -776,47 +775,48 @@ impl Render for LspLogToolbarItemView { ); } - menu = menu.custom_entry({ - let log_view = log_view.clone(); - let log_toolbar_view = log_toolbar_view.clone(); - move |cx| { - h_stack() - .w_full() - .justify_between() - .child(Label::new(RPC_MESSAGES)) - .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, - ); - }, + 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(); + }, + )), ), - ), - ) - .on_mouse_down( - MouseButton::Left, - cx.listener_for(&log_view, move |view, _, cx| { - view.show_rpc_trace_for_server(row.server_id, cx); - }), - ) - .into_any_element() - } - }); + ) + .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), diff --git a/crates/ui2/src/components/context_menu.rs b/crates/ui2/src/components/context_menu.rs index 1fda44e86d7f7529de4c3b014d12ce5982dd9e26..97bb3865ef6dbefd95e4274fe77d37df630aee74 100644 --- a/crates/ui2/src/components/context_menu.rs +++ b/crates/ui2/src/components/context_menu.rs @@ -20,6 +20,7 @@ enum ContextMenuItem { }, CustomEntry { entry_render: Box AnyElement>, + handler: Rc, }, } @@ -89,9 +90,11 @@ impl ContextMenu { 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 } @@ -117,10 +120,12 @@ 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); @@ -251,51 +256,56 @@ impl Render for ContextMenu { }) .flex_none() .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(); - - 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() - }; + |(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(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)) - })), - ) + let label_element = if let Some(icon) = icon { + h_stack() + .gap_1() + .child(Label::new(label.clone())) + .child(IconElement::new(*icon)) .into_any_element() - } - ContextMenuItem::CustomEntry { entry_render } => ListItem::new(ix) + } 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(), + .into_any_element() } }, ))), From fc923a2a98044cb31c8739b9e63092edd77500c7 Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Wed, 20 Dec 2023 18:28:32 +0200 Subject: [PATCH 14/14] Close syntax tree view menu on click too --- crates/language_tools2/src/syntax_tree_view.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/language_tools2/src/syntax_tree_view.rs b/crates/language_tools2/src/syntax_tree_view.rs index f0b03422cb25251faf6d5e0d6bfdcecb6aa810cd..dcdcb612b781e6d93e92650c111f9ea9e1ac9d1f 100644 --- a/crates/language_tools2/src/syntax_tree_view.rs +++ b/crates/language_tools2/src/syntax_tree_view.rs @@ -480,6 +480,7 @@ impl SyntaxTreeToolbarItemView { buffer_state.active_layer = Some(layer.to_owned()); view.selected_descendant_ix = None; cx.notify(); + view.focus_handle.focus(cx); Some(()) }) }