From 64925231b0b3a6a40da37fd8d9b124bc3a6abaad Mon Sep 17 00:00:00 2001 From: Kirill Bulatov Date: Thu, 14 Dec 2023 15:20:49 +0200 Subject: [PATCH] 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 cb0d54022f..4f418a1813 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 3b453527b8..42432a8a2a 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 0000000000..ca3ede2ef9 --- /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 0000000000..0a1f31f03f --- /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 0000000000..9f27c62a04 --- /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 0000000000..967e8a3382 --- /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 0000000000..7eaaab0130 --- /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 + } +}