From 086cfe57c5183ffb0ed66af1a6b0e16c313fcdac Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 9 Jun 2023 14:55:46 -0700 Subject: [PATCH 1/6] Start work on a syntax tree view --- Cargo.lock | 51 +-- Cargo.toml | 5 +- crates/editor/Cargo.toml | 2 +- crates/editor/src/multi_buffer.rs | 8 +- crates/language/Cargo.toml | 2 +- crates/language/src/buffer.rs | 15 +- crates/language/src/buffer_tests.rs | 2 +- crates/language/src/language.rs | 1 + crates/language/src/syntax_map.rs | 94 ++++-- crates/{lsp_log => language_tools}/Cargo.toml | 5 +- crates/language_tools/src/language_tools.rs | 15 + .../src/lsp_log.rs | 27 +- .../src/lsp_log_tests.rs | 9 +- crates/language_tools/src/syntax_tree_view.rs | 297 ++++++++++++++++++ crates/settings/Cargo.toml | 2 +- crates/zed/Cargo.toml | 4 +- crates/zed/src/main.rs | 2 +- crates/zed/src/zed.rs | 2 +- 18 files changed, 446 insertions(+), 97 deletions(-) rename crates/{lsp_log => language_tools}/Cargo.toml (90%) create mode 100644 crates/language_tools/src/language_tools.rs rename crates/{lsp_log => language_tools}/src/lsp_log.rs (98%) rename crates/{lsp_log => language_tools}/src/lsp_log_tests.rs (95%) create mode 100644 crates/language_tools/src/syntax_tree_view.rs diff --git a/Cargo.lock b/Cargo.lock index 8ef48849acb94fe864b9af31d2999fc4c9e05271..711c97df409334919357eca937fa3643c4d1c338 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3515,6 +3515,29 @@ dependencies = [ "workspace", ] +[[package]] +name = "language_tools" +version = "0.1.0" +dependencies = [ + "anyhow", + "client", + "collections", + "editor", + "env_logger 0.9.3", + "futures 0.3.28", + "gpui", + "language", + "lsp", + "project", + "serde", + "settings", + "theme", + "tree-sitter", + "unindent", + "util", + "workspace", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -3759,28 +3782,6 @@ dependencies = [ "url", ] -[[package]] -name = "lsp_log" -version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "editor", - "env_logger 0.9.3", - "futures 0.3.28", - "gpui", - "language", - "lsp", - "project", - "serde", - "settings", - "theme", - "unindent", - "util", - "workspace", -] - [[package]] name = "mach" version = "0.3.2" @@ -7358,8 +7359,8 @@ dependencies = [ [[package]] name = "tree-sitter" -version = "0.20.9" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" +version = "0.20.10" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=a2119cb6914d62e626fcc40684ef900d7fa90d86#a2119cb6914d62e626fcc40684ef900d7fa90d86" dependencies = [ "cc", "regex", @@ -8829,11 +8830,11 @@ dependencies = [ "journal", "language", "language_selector", + "language_tools", "lazy_static", "libc", "log", "lsp", - "lsp_log", "node_runtime", "num_cpus", "outline", diff --git a/Cargo.toml b/Cargo.toml index 72a93177a9677e52f7cb0399dbc936fd584f6061..e8003684c3179cf35c768bbb210bb5d4ff28cc2f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,10 +32,10 @@ members = [ "crates/journal", "crates/language", "crates/language_selector", + "crates/language_tools", "crates/live_kit_client", "crates/live_kit_server", "crates/lsp", - "crates/lsp_log", "crates/media", "crates/menu", "crates/node_runtime", @@ -98,10 +98,11 @@ tempdir = { version = "0.3.7" } thiserror = { version = "1.0.29" } time = { version = "0.3", features = ["serde", "serde-well-known"] } toml = { version = "0.5" } +tree-sitter = "0.20" unindent = { version = "0.1.7" } [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "a2119cb6914d62e626fcc40684ef900d7fa90d86" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 diff --git a/crates/editor/Cargo.toml b/crates/editor/Cargo.toml index 325883b7c03fcbc8ed4d2c33df7f4ff00d4e3ef2..dcc22202273a5d087a5892f279e1d153b3d9770e 100644 --- a/crates/editor/Cargo.toml +++ b/crates/editor/Cargo.toml @@ -83,7 +83,7 @@ ctor.workspace = true env_logger.workspace = true rand.workspace = true unindent.workspace = true -tree-sitter = "0.20" +tree-sitter.workspace = true tree-sitter-rust = "0.20" tree-sitter-html = "0.19" tree-sitter-typescript = { git = "https://github.com/tree-sitter/tree-sitter-typescript", rev = "5d20856f34315b068c41edaee2ac8a100081d259" } diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 0d7fb6a450d9ee784c1c601450d2170939a7e073..4c7736e2701072ca1881fc993bf760352ed56c01 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1118,7 +1118,7 @@ impl MultiBuffer { &self, point: T, cx: &AppContext, - ) -> Option<(ModelHandle, usize)> { + ) -> Option<(ModelHandle, usize, ExcerptId)> { let snapshot = self.read(cx); let offset = point.to_offset(&snapshot); let mut cursor = snapshot.excerpts.cursor::(); @@ -1132,7 +1132,7 @@ impl MultiBuffer { let buffer_point = excerpt_start + offset - *cursor.start(); let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); - (buffer, buffer_point) + (buffer, buffer_point, excerpt.id) }) } @@ -1387,7 +1387,7 @@ impl MultiBuffer { cx: &'a AppContext, ) -> Option> { self.point_to_buffer_offset(point, cx) - .and_then(|(buffer, offset)| buffer.read(cx).language_at(offset)) + .and_then(|(buffer, offset, _)| buffer.read(cx).language_at(offset)) } pub fn settings_at<'a, T: ToOffset>( @@ -1397,7 +1397,7 @@ impl MultiBuffer { ) -> &'a LanguageSettings { let mut language = None; let mut file = None; - if let Some((buffer, offset)) = self.point_to_buffer_offset(point, cx) { + if let Some((buffer, offset, _)) = self.point_to_buffer_offset(point, cx) { let buffer = buffer.read(cx); language = buffer.language_at(offset); file = buffer.file(); diff --git a/crates/language/Cargo.toml b/crates/language/Cargo.toml index 9722b618f3a135eb365699769150b06b16086624..04b48c93ff7b305b77c523b9c041c393116ca1b2 100644 --- a/crates/language/Cargo.toml +++ b/crates/language/Cargo.toml @@ -55,7 +55,7 @@ serde_json.workspace = true similar = "1.3" smallvec.workspace = true smol.workspace = true -tree-sitter = "0.20" +tree-sitter.workspace = true tree-sitter-rust = { version = "*", optional = true } tree-sitter-typescript = { version = "*", optional = true } unicase = "2.6" diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index e09ee48da630989774abdbf0bdffd386afa44a1b..d9efb0fbc674ec84ab352f4d8922cd1e557399e7 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -8,7 +8,8 @@ use crate::{ language_settings::{language_settings, LanguageSettings}, outline::OutlineItem, syntax_map::{ - SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, ToTreeSitterPoint, + SyntaxLayerInfo, SyntaxMap, SyntaxMapCapture, SyntaxMapCaptures, SyntaxSnapshot, + ToTreeSitterPoint, }, CodeLabel, LanguageScope, Outline, }; @@ -2116,12 +2117,16 @@ impl BufferSnapshot { } } - pub fn language_at(&self, position: D) -> Option<&Arc> { + pub fn syntax_layer_at(&self, position: D) -> Option { let offset = position.to_offset(self); self.syntax .layers_for_range(offset..offset, &self.text) - .filter(|l| l.node.end_byte() > offset) + .filter(|l| l.node().end_byte() > offset) .last() + } + + pub fn language_at(&self, position: D) -> Option<&Arc> { + self.syntax_layer_at(position) .map(|info| info.language) .or(self.language.as_ref()) } @@ -2140,7 +2145,7 @@ impl BufferSnapshot { if let Some(layer_info) = self .syntax .layers_for_range(offset..offset, &self.text) - .filter(|l| l.node.end_byte() > offset) + .filter(|l| l.node().end_byte() > offset) .last() { Some(LanguageScope { @@ -2188,7 +2193,7 @@ impl BufferSnapshot { let range = range.start.to_offset(self)..range.end.to_offset(self); let mut result: Option> = None; 'outer: for layer in self.syntax.layers_for_range(range.clone(), &self.text) { - let mut cursor = layer.node.walk(); + let mut cursor = layer.node().walk(); // Descend to the first leaf that touches the start of the range, // and if the range is non-empty, extends beyond the start. diff --git a/crates/language/src/buffer_tests.rs b/crates/language/src/buffer_tests.rs index 9f44de40ac1f4010f7e335277c38e594650f0140..38cefbcef9393ef577f67829febca7bbf03a6358 100644 --- a/crates/language/src/buffer_tests.rs +++ b/crates/language/src/buffer_tests.rs @@ -2242,7 +2242,7 @@ fn get_tree_sexp(buffer: &ModelHandle, cx: &gpui::TestAppContext) -> Str buffer.read_with(cx, |buffer, _| { let snapshot = buffer.snapshot(); let layers = snapshot.syntax.layers(buffer.as_text_snapshot()); - layers[0].node.to_sexp() + layers[0].node().to_sexp() }) } diff --git a/crates/language/src/language.rs b/crates/language/src/language.rs index 0ff1d973d3ff47d7e67a82a7639ddcac810702eb..e91d5770cfba80485f3cbe6272257a81c48c4fc3 100644 --- a/crates/language/src/language.rs +++ b/crates/language/src/language.rs @@ -57,6 +57,7 @@ pub use buffer::*; pub use diagnostic_set::DiagnosticEntry; pub use lsp::LanguageServerId; pub use outline::{Outline, OutlineItem}; +pub use syntax_map::{OwnedSyntaxLayerInfo, SyntaxLayerInfo}; pub use tree_sitter::{Parser, Tree}; pub fn init(cx: &mut AppContext) { diff --git a/crates/language/src/syntax_map.rs b/crates/language/src/syntax_map.rs index 16313db49881070f6c0baac9b71b317761079830..ebfa38c148dfb7c28e51821ad5339ae627e52c6d 100644 --- a/crates/language/src/syntax_map.rs +++ b/crates/language/src/syntax_map.rs @@ -125,8 +125,17 @@ impl SyntaxLayerContent { #[derive(Debug)] pub struct SyntaxLayerInfo<'a> { pub depth: usize, - pub node: Node<'a>, pub language: &'a Arc, + tree: &'a Tree, + offset: (usize, tree_sitter::Point), +} + +#[derive(Clone)] +pub struct OwnedSyntaxLayerInfo { + pub depth: usize, + pub language: Arc, + tree: tree_sitter::Tree, + offset: (usize, tree_sitter::Point), } #[derive(Debug, Clone)] @@ -664,8 +673,9 @@ impl SyntaxSnapshot { text, [SyntaxLayerInfo { language, + tree, depth: 0, - node: tree.root_node(), + offset: (0, tree_sitter::Point::new(0, 0)), }] .into_iter(), query, @@ -728,9 +738,10 @@ impl SyntaxSnapshot { while let Some(layer) = cursor.item() { if let SyntaxLayerContent::Parsed { tree, language } = &layer.content { let info = SyntaxLayerInfo { + tree, language, depth: layer.depth, - node: tree.root_node_with_offset( + offset: ( layer.range.start.to_offset(buffer), layer.range.start.to_point(buffer).to_ts_point(), ), @@ -766,13 +777,8 @@ impl<'a> SyntaxMapCaptures<'a> { grammars: Vec::new(), active_layer_count: 0, }; - for SyntaxLayerInfo { - language, - depth, - node, - } in layers - { - let grammar = match &language.grammar { + for layer in layers { + let grammar = match &layer.language.grammar { Some(grammar) => grammar, None => continue, }; @@ -789,7 +795,7 @@ impl<'a> SyntaxMapCaptures<'a> { }; cursor.set_byte_range(range.clone()); - let captures = cursor.captures(query, node, TextProvider(text)); + let captures = cursor.captures(query, layer.node(), TextProvider(text)); let grammar_index = result .grammars .iter() @@ -799,7 +805,7 @@ impl<'a> SyntaxMapCaptures<'a> { result.grammars.len() - 1 }); let mut layer = SyntaxMapCapturesLayer { - depth, + depth: layer.depth, grammar_index, next_capture: None, captures, @@ -889,13 +895,8 @@ impl<'a> SyntaxMapMatches<'a> { query: fn(&Grammar) -> Option<&Query>, ) -> Self { let mut result = Self::default(); - for SyntaxLayerInfo { - language, - depth, - node, - } in layers - { - let grammar = match &language.grammar { + for layer in layers { + let grammar = match &layer.language.grammar { Some(grammar) => grammar, None => continue, }; @@ -912,7 +913,7 @@ impl<'a> SyntaxMapMatches<'a> { }; cursor.set_byte_range(range.clone()); - let matches = cursor.matches(query, node, TextProvider(text)); + let matches = cursor.matches(query, layer.node(), TextProvider(text)); let grammar_index = result .grammars .iter() @@ -922,7 +923,7 @@ impl<'a> SyntaxMapMatches<'a> { result.grammars.len() - 1 }); let mut layer = SyntaxMapMatchesLayer { - depth, + depth: layer.depth, grammar_index, matches, next_pattern_index: 0, @@ -1290,7 +1291,28 @@ fn splice_included_ranges( ranges } +impl OwnedSyntaxLayerInfo { + pub fn node(&self) -> Node { + self.tree + .root_node_with_offset(self.offset.0, self.offset.1) + } +} + impl<'a> SyntaxLayerInfo<'a> { + pub fn to_owned(&self) -> OwnedSyntaxLayerInfo { + OwnedSyntaxLayerInfo { + tree: self.tree.clone(), + offset: self.offset, + depth: self.depth, + language: self.language.clone(), + } + } + + pub fn node(&self) -> Node<'a> { + self.tree + .root_node_with_offset(self.offset.0, self.offset.1) + } + pub(crate) fn override_id(&self, offset: usize, text: &text::BufferSnapshot) -> Option { let text = TextProvider(text.as_rope()); let config = self.language.grammar.as_ref()?.override_config.as_ref()?; @@ -1299,7 +1321,7 @@ impl<'a> SyntaxLayerInfo<'a> { query_cursor.set_byte_range(offset..offset); let mut smallest_match: Option<(u32, Range)> = None; - for mat in query_cursor.matches(&config.query, self.node, text) { + for mat in query_cursor.matches(&config.query, self.node(), text) { for capture in mat.captures { if !config.values.contains_key(&capture.index) { continue; @@ -2328,8 +2350,11 @@ mod tests { let reference_layers = reference_syntax_map.layers(&buffer); for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) { - assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp()); - assert_eq!(edited_layer.node.range(), reference_layer.node.range()); + assert_eq!( + edited_layer.node().to_sexp(), + reference_layer.node().to_sexp() + ); + assert_eq!(edited_layer.node().range(), reference_layer.node().range()); } } @@ -2411,8 +2436,11 @@ mod tests { let reference_layers = reference_syntax_map.layers(&buffer); for (edited_layer, reference_layer) in layers.into_iter().zip(reference_layers.into_iter()) { - assert_eq!(edited_layer.node.to_sexp(), reference_layer.node.to_sexp()); - assert_eq!(edited_layer.node.range(), reference_layer.node.range()); + assert_eq!( + edited_layer.node().to_sexp(), + reference_layer.node().to_sexp() + ); + assert_eq!(edited_layer.node().range(), reference_layer.node().range()); } } @@ -2563,13 +2591,13 @@ mod tests { mutated_layers.into_iter().zip(reference_layers.into_iter()) { assert_eq!( - edited_layer.node.to_sexp(), - reference_layer.node.to_sexp(), + edited_layer.node().to_sexp(), + reference_layer.node().to_sexp(), "different layer at step {i}" ); assert_eq!( - edited_layer.node.range(), - reference_layer.node.range(), + edited_layer.node().range(), + reference_layer.node().range(), "different layer at step {i}" ); } @@ -2709,10 +2737,8 @@ mod tests { expected_layers.len(), "wrong number of layers" ); - for (i, (SyntaxLayerInfo { node, .. }, expected_s_exp)) in - layers.iter().zip(expected_layers.iter()).enumerate() - { - let actual_s_exp = node.to_sexp(); + for (i, (layer, expected_s_exp)) in layers.iter().zip(expected_layers.iter()).enumerate() { + let actual_s_exp = layer.node().to_sexp(); assert!( string_contains_sequence( &actual_s_exp, diff --git a/crates/lsp_log/Cargo.toml b/crates/language_tools/Cargo.toml similarity index 90% rename from crates/lsp_log/Cargo.toml rename to crates/language_tools/Cargo.toml index 46f6006a23efde3569311da8f936c65e40ea9a93..e67a4b36df761fd9ac23b95d83b3fa43b78c6da1 100644 --- a/crates/lsp_log/Cargo.toml +++ b/crates/language_tools/Cargo.toml @@ -1,11 +1,11 @@ [package] -name = "lsp_log" +name = "language_tools" version = "0.1.0" edition = "2021" publish = false [lib] -path = "src/lsp_log.rs" +path = "src/language_tools.rs" doctest = false [dependencies] @@ -22,6 +22,7 @@ lsp = { path = "../lsp" } futures.workspace = true serde.workspace = true anyhow.workspace = true +tree-sitter.workspace = true [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs new file mode 100644 index 0000000000000000000000000000000000000000..b61c8d3c80a0c9b34fe213288d2b6dba80bb2391 --- /dev/null +++ b/crates/language_tools/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::SyntaxTreeView; + +pub fn init(cx: &mut AppContext) { + lsp_log::init(cx); + syntax_tree_view::init(cx); +} diff --git a/crates/lsp_log/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs similarity index 98% rename from crates/lsp_log/src/lsp_log.rs rename to crates/language_tools/src/lsp_log.rs index 5808b4da2ed0e0b0b294f8840477064477c8f118..cec690d4a6fd09cffbc701196e9752672370fab4 100644 --- a/crates/lsp_log/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -1,6 +1,3 @@ -#[cfg(test)] -mod lsp_log_tests; - use collections::HashMap; use editor::Editor; use futures::{channel::mpsc, StreamExt}; @@ -27,7 +24,7 @@ use workspace::{ const SEND_LINE: &str = "// Send:\n"; const RECEIVE_LINE: &str = "// Receive:\n"; -struct LogStore { +pub struct LogStore { projects: HashMap, ProjectState>, io_tx: mpsc::UnboundedSender<(WeakModelHandle, LanguageServerId, bool, String)>, } @@ -49,10 +46,10 @@ struct LanguageServerRpcState { } pub struct LspLogView { + pub(crate) editor: ViewHandle, log_store: ModelHandle, current_server_id: Option, is_showing_rpc_trace: bool, - editor: ViewHandle, project: ModelHandle, } @@ -68,13 +65,13 @@ enum MessageKind { } #[derive(Clone, Debug, PartialEq)] -struct LogMenuItem { - server_id: LanguageServerId, - server_name: LanguageServerName, - worktree: ModelHandle, - rpc_trace_enabled: bool, - rpc_trace_selected: bool, - logs_selected: bool, +pub(crate) struct LogMenuItem { + pub server_id: LanguageServerId, + pub server_name: LanguageServerName, + pub worktree: ModelHandle, + pub rpc_trace_enabled: bool, + pub rpc_trace_selected: bool, + pub logs_selected: bool, } actions!(log, [OpenLanguageServerLogs]); @@ -114,7 +111,7 @@ pub fn init(cx: &mut AppContext) { } impl LogStore { - fn new(cx: &mut ModelContext) -> Self { + pub fn new(cx: &mut ModelContext) -> Self { let (io_tx, mut io_rx) = mpsc::unbounded(); let this = Self { projects: HashMap::default(), @@ -320,7 +317,7 @@ impl LogStore { } impl LspLogView { - fn new( + pub fn new( project: ModelHandle, log_store: ModelHandle, cx: &mut ViewContext, @@ -360,7 +357,7 @@ impl LspLogView { editor } - fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option> { + 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 diff --git a/crates/lsp_log/src/lsp_log_tests.rs b/crates/language_tools/src/lsp_log_tests.rs similarity index 95% rename from crates/lsp_log/src/lsp_log_tests.rs rename to crates/language_tools/src/lsp_log_tests.rs index 572758ad63bbd0ff7c8a964fab4f1878baab205f..d4a16b5758a2a905c0e621b368a9653114828ec4 100644 --- a/crates/lsp_log/src/lsp_log_tests.rs +++ b/crates/language_tools/src/lsp_log_tests.rs @@ -1,7 +1,12 @@ +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}; -use project::FakeFs; +use language::{tree_sitter_rust, FakeLspAdapter, Language, LanguageConfig, LanguageServerName}; +use project::{FakeFs, Project}; use settings::SettingsStore; #[gpui::test] diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs new file mode 100644 index 0000000000000000000000000000000000000000..96a25cc7129212952e5865955fcf40b644260e01 --- /dev/null +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -0,0 +1,297 @@ +use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; +use gpui::{ + actions, + elements::{Empty, Label, MouseEventHandler, UniformList, UniformListState}, + fonts::TextStyle, + platform::MouseButton, + AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, +}; +use language::{Buffer, OwnedSyntaxLayerInfo}; +use std::ops::Range; +use theme::ThemeSettings; +use workspace::{ + item::{Item, ItemHandle}, + Workspace, +}; + +actions!(log, [OpenSyntaxTreeView]); + +pub fn init(cx: &mut AppContext) { + cx.add_action( + move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| { + let syntax_tree_view = cx.add_view(|cx| SyntaxTreeView::new(workspace, cx)); + workspace.add_item(Box::new(syntax_tree_view), cx); + }, + ); +} + +pub struct SyntaxTreeView { + editor: Option<(ViewHandle, gpui::Subscription)>, + buffer: Option<(ModelHandle, usize, ExcerptId)>, + layer: Option, + hover_y: Option, + line_height: Option, + list_state: UniformListState, + active_descendant_ix: Option, + highlighted_active_descendant: bool, +} + +impl SyntaxTreeView { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let mut this = Self { + list_state: UniformListState::default(), + editor: None, + buffer: None, + layer: None, + hover_y: None, + line_height: None, + active_descendant_ix: None, + highlighted_active_descendant: false, + }; + + this.workspace_updated(workspace.active_item(cx), cx); + cx.observe( + &workspace.weak_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((current_editor, _)) = &self.editor { + if current_editor == &editor { + return; + } + editor.update(cx, |editor, cx| { + editor.clear_background_highlights::(cx); + }); + } + + let subscription = cx.subscribe(&editor, |this, editor, event, cx| { + let selection_changed = match event { + editor::Event::Reparsed => false, + editor::Event::SelectionsChanged { .. } => true, + _ => return, + }; + this.editor_updated(&editor, selection_changed, cx); + }); + + self.editor_updated(&editor, true, cx); + self.editor = Some((editor, subscription)); + } + + fn editor_updated( + &mut self, + editor: &ViewHandle, + selection_changed: bool, + cx: &mut ViewContext, + ) { + let editor = editor.read(cx); + if selection_changed { + let cursor = editor.selections.last::(cx).end; + self.buffer = editor.buffer().read(cx).point_to_buffer_offset(cursor, cx); + self.layer = self.buffer.as_ref().and_then(|(buffer, offset, _)| { + buffer + .read(cx) + .snapshot() + .syntax_layer_at(*offset) + .map(|l| l.to_owned()) + }); + } + cx.notify(); + } + + fn hover_state_changed(&mut self, cx: &mut ViewContext) { + if let Some((y, line_height)) = self.hover_y.zip(self.line_height) { + let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; + if self.active_descendant_ix != Some(ix) { + self.active_descendant_ix = Some(ix); + self.highlighted_active_descendant = false; + cx.notify(); + } + } + } + + fn handle_click(&mut self, y: f32, cx: &mut ViewContext) { + if let Some(line_height) = self.line_height { + let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; + if let Some(layer) = &self.layer { + let mut cursor = layer.node().walk(); + cursor.goto_descendant(ix); + let node = cursor.node(); + self.update_editor_with_node_range(node, cx, |editor, range, cx| { + editor.change_selections(Some(Autoscroll::newest()), cx, |selections| { + selections.select_ranges(vec![range]); + }); + }); + } + } + } + + fn update_editor_with_node_range( + &self, + node: tree_sitter::Node, + cx: &mut ViewContext, + mut f: impl FnMut(&mut Editor, Range, &mut ViewContext), + ) { + let range = node.byte_range(); + if let Some((editor, _)) = &self.editor { + if let Some((buffer, _, excerpt_id)) = &self.buffer { + let buffer = &buffer.read(cx); + let multibuffer = editor.read(cx).buffer(); + let multibuffer = multibuffer.read(cx).snapshot(cx); + let start = + multibuffer.anchor_in_excerpt(*excerpt_id, buffer.anchor_before(range.start)); + let end = + multibuffer.anchor_in_excerpt(*excerpt_id, buffer.anchor_after(range.end)); + editor.update(cx, |editor, cx| { + f(editor, start..end, cx); + }); + } + } + } + + fn node_is_active(&mut self, node: tree_sitter::Node, cx: &mut ViewContext) { + if self.highlighted_active_descendant { + return; + } + self.highlighted_active_descendant = true; + self.update_editor_with_node_range(node, cx, |editor, range, cx| { + editor.clear_background_highlights::(cx); + editor.highlight_background::( + vec![range], + |theme| theme.editor.document_highlight_write_background, + cx, + ); + }); + } +} + +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(), + }; + self.line_height = Some(cx.font_cache().line_height(font_size)); + + self.hover_state_changed(cx); + + if let Some(layer) = &self.layer { + let layer = layer.clone(); + return MouseEventHandler::::new(0, cx, move |_, cx| { + 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 { + let node = cursor.node(); + let is_hovered = Some(descendant_ix) == this.active_descendant_ix; + if is_hovered { + this.node_is_active(node, cx); + } + items.push( + Label::new(node.kind(), style.clone()) + .contained() + .with_background_color(if is_hovered { + editor_theme.active_line_background + } else { + Default::default() + }) + .with_padding_left(depth as f32 * 10.0) + .into_any(), + ); + 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.hover_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); + }) + .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() + } +} diff --git a/crates/settings/Cargo.toml b/crates/settings/Cargo.toml index f0396266fc78ff3ce515ce63350f98aa5bc9d2d6..dab4b919923678bf73ec588e5affb7cf48d8bda4 100644 --- a/crates/settings/Cargo.toml +++ b/crates/settings/Cargo.toml @@ -31,7 +31,7 @@ serde_derive.workspace = true serde_json.workspace = true smallvec.workspace = true toml.workspace = true -tree-sitter = "*" +tree-sitter.workspace = true tree-sitter-json = "*" [dev-dependencies] diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index 8fba2a302cdfcdd71967e0c0be26f4323af6a8a3..2a7274990b76650d188b61c430a3ba201ff6ab7f 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -45,7 +45,7 @@ journal = { path = "../journal" } language = { path = "../language" } language_selector = { path = "../language_selector" } lsp = { path = "../lsp" } -lsp_log = { path = "../lsp_log" } +language_tools = { path = "../language_tools" } node_runtime = { path = "../node_runtime" } ai = { path = "../ai" } outline = { path = "../outline" } @@ -102,7 +102,7 @@ tempdir.workspace = true thiserror.workspace = true tiny_http = "0.8" toml.workspace = true -tree-sitter = "0.20" +tree-sitter.workspace = true tree-sitter-c = "0.20.1" tree-sitter-cpp = "0.20.0" tree-sitter-css = { git = "https://github.com/tree-sitter/tree-sitter-css", rev = "769203d0f9abe1a9a691ac2b9fe4bb4397a73c51" } diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2393d0df3b1ce38bd41c56786ad8d232d5734b6b..73a3346a9adc75b61af3ab4e5f50b4484129bb1b 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -191,7 +191,7 @@ fn main() { language_selector::init(cx); theme_selector::init(cx); activity_indicator::init(cx); - lsp_log::init(cx); + language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); feedback::init(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index d15bace5545d4087ae708adf7b0504838cfe6e54..cef6cc7d3c35a0d88e6f804ea196c03fa0c01c06 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -312,7 +312,7 @@ pub fn initialize_workspace( let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); toolbar.add_item(feedback_info_text, cx); let lsp_log_item = - cx.add_view(|_| lsp_log::LspLogToolbarItemView::new()); + cx.add_view(|_| language_tools::LspLogToolbarItemView::new()); toolbar.add_item(lsp_log_item, cx); }) }); From e969e3b0288ca4a12288a78f0d622168363ba99a Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 9 Jun 2023 16:11:29 -0700 Subject: [PATCH 2/6] Sync selection between syntax tree view and editor --- crates/editor/src/editor.rs | 2 +- crates/editor/src/multi_buffer.rs | 6 +- crates/language_tools/src/syntax_tree_view.rs | 262 +++++++++++------- 3 files changed, 167 insertions(+), 103 deletions(-) diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index cecefc7061eeb22791497db76af0ccad7af6cded..9562d18df495a50451768a0a156086f6184c4764 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -7102,7 +7102,7 @@ impl Editor { let mut new_selections_by_buffer = HashMap::default(); for selection in editor.selections.all::(cx) { - for (buffer, mut range) in + for (buffer, mut range, _) in buffer.range_to_buffer_ranges(selection.start..selection.end, cx) { if selection.reversed { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 4c7736e2701072ca1881fc993bf760352ed56c01..955902da1263f875c6e3c3779b6cf39d84ccc191 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1140,7 +1140,7 @@ impl MultiBuffer { &self, range: Range, cx: &AppContext, - ) -> Vec<(ModelHandle, Range)> { + ) -> Vec<(ModelHandle, Range, ExcerptId)> { let snapshot = self.read(cx); let start = range.start.to_offset(&snapshot); let end = range.end.to_offset(&snapshot); @@ -1165,7 +1165,7 @@ impl MultiBuffer { let start = excerpt_start + (cmp::max(start, *cursor.start()) - *cursor.start()); let end = excerpt_start + (cmp::min(end, end_before_newline) - *cursor.start()); let buffer = self.buffers.borrow()[&excerpt.buffer_id].buffer.clone(); - result.push((buffer, start..end)); + result.push((buffer, start..end, excerpt.id)); cursor.next(&()); } @@ -5196,7 +5196,7 @@ mod tests { .range_to_buffer_ranges(start_ix..end_ix, cx); let excerpted_buffers_text = excerpted_buffer_ranges .iter() - .map(|(buffer, buffer_range)| { + .map(|(buffer, buffer_range, _)| { buffer .read(cx) .text_for_range(buffer_range.clone()) diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 96a25cc7129212952e5865955fcf40b644260e01..781961af07b08ce636ccc472e6970cf724a01daa 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -1,7 +1,7 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ actions, - elements::{Empty, Label, MouseEventHandler, UniformList, UniformListState}, + elements::{Empty, Label, MouseEventHandler, ScrollTarget, UniformList, UniformListState}, fonts::TextStyle, platform::MouseButton, AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, @@ -26,14 +26,24 @@ pub fn init(cx: &mut AppContext) { } pub struct SyntaxTreeView { - editor: Option<(ViewHandle, gpui::Subscription)>, - buffer: Option<(ModelHandle, usize, ExcerptId)>, - layer: Option, - hover_y: Option, + editor: Option, + mouse_y: Option, line_height: Option, list_state: UniformListState, - active_descendant_ix: Option, - highlighted_active_descendant: bool, + selected_descendant_ix: Option, + hovered_descendant_ix: Option, +} + +struct EditorState { + editor: ViewHandle, + active_buffer: Option, + _subscription: gpui::Subscription, +} + +struct BufferState { + buffer: ModelHandle, + excerpt_id: ExcerptId, + active_layer: Option, } impl SyntaxTreeView { @@ -41,12 +51,10 @@ impl SyntaxTreeView { let mut this = Self { list_state: UniformListState::default(), editor: None, - buffer: None, - layer: None, - hover_y: None, + mouse_y: None, line_height: None, - active_descendant_ix: None, - highlighted_active_descendant: false, + hovered_descendant_ix: None, + selected_descendant_ix: None, }; this.workspace_updated(workspace.active_item(cx), cx); @@ -76,112 +84,157 @@ impl SyntaxTreeView { } fn set_editor(&mut self, editor: ViewHandle, cx: &mut ViewContext) { - if let Some((current_editor, _)) = &self.editor { - if current_editor == &editor { + if let Some(state) = &self.editor { + if state.editor == editor { return; } editor.update(cx, |editor, cx| { - editor.clear_background_highlights::(cx); + editor.clear_background_highlights::(cx) }); } - let subscription = cx.subscribe(&editor, |this, editor, event, cx| { - let selection_changed = match event { - editor::Event::Reparsed => false, - editor::Event::SelectionsChanged { .. } => true, + let subscription = cx.subscribe(&editor, |this, _, event, cx| { + let reset_layer = match event { + editor::Event::Reparsed => true, + editor::Event::SelectionsChanged { .. } => false, _ => return, }; - this.editor_updated(&editor, selection_changed, cx); + this.editor_updated(reset_layer, cx); }); - self.editor_updated(&editor, true, cx); - self.editor = Some((editor, subscription)); + self.editor = Some(EditorState { + editor, + _subscription: subscription, + active_buffer: None, + }); + self.editor_updated(true, cx); } - fn editor_updated( - &mut self, - editor: &ViewHandle, - selection_changed: bool, - cx: &mut ViewContext, - ) { - let editor = editor.read(cx); - if selection_changed { - let cursor = editor.selections.last::(cx).end; - self.buffer = editor.buffer().read(cx).point_to_buffer_offset(cursor, cx); - self.layer = self.buffer.as_ref().and_then(|(buffer, offset, _)| { - buffer - .read(cx) - .snapshot() - .syntax_layer_at(*offset) - .map(|l| l.to_owned()) + fn editor_updated(&mut self, reset_layer: 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, }); + if reset_layer + || 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; } - cx.notify(); - } - fn hover_state_changed(&mut self, cx: &mut ViewContext) { - if let Some((y, line_height)) = self.hover_y.zip(self.line_height) { - let ix = ((self.list_state.scroll_top() + y) / line_height) as usize; - if self.active_descendant_ix != Some(ix) { - self.active_descendant_ix = Some(ix); - self.highlighted_active_descendant = false; - cx.notify(); + // Within the active layer, find the syntax node under the cursor, + // and scroll to it. + let layer = match &mut buffer_state.active_layer { + Some(layer) => layer, + None => { + let layer = buffer.read(cx).snapshot().syntax_layer_at(0)?.to_owned(); + buffer_state.active_layer.insert(layer) + } + }; + 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, range, cx| { + editor.change_selections(Some(Autoscroll::newest()), cx, |selections| { + selections.select_ranges(vec![range]); + }); + }); + Some(()) } - fn handle_click(&mut self, y: f32, cx: &mut ViewContext) { - if let Some(line_height) = self.line_height { + 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 let Some(layer) = &self.layer { - let mut cursor = layer.node().walk(); - cursor.goto_descendant(ix); - let node = cursor.node(); - self.update_editor_with_node_range(node, cx, |editor, range, cx| { - editor.change_selections(Some(Autoscroll::newest()), cx, |selections| { - selections.select_ranges(vec![range]); - }); + 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_node_range( + fn update_editor_with_range_for_descendant_ix( &self, - node: tree_sitter::Node, + 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(); - if let Some((editor, _)) = &self.editor { - if let Some((buffer, _, excerpt_id)) = &self.buffer { - let buffer = &buffer.read(cx); - let multibuffer = editor.read(cx).buffer(); - let multibuffer = multibuffer.read(cx).snapshot(cx); - let start = - multibuffer.anchor_in_excerpt(*excerpt_id, buffer.anchor_before(range.start)); - let end = - multibuffer.anchor_in_excerpt(*excerpt_id, buffer.anchor_after(range.end)); - editor.update(cx, |editor, cx| { - f(editor, start..end, cx); - }); - } - } - } - fn node_is_active(&mut self, node: tree_sitter::Node, cx: &mut ViewContext) { - if self.highlighted_active_descendant { - return; - } - self.highlighted_active_descendant = true; - self.update_editor_with_node_range(node, cx, |editor, range, cx| { - editor.clear_background_highlights::(cx); - editor.highlight_background::( - vec![range], - |theme| theme.editor.document_highlight_write_background, - cx, - ); + // 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(()) } } @@ -215,18 +268,27 @@ impl View for SyntaxTreeView { font_properties: Default::default(), underline: Default::default(), }; - self.line_height = Some(cx.font_cache().line_height(font_size)); - self.hover_state_changed(cx); + let line_height = Some(cx.font_cache().line_height(font_size)); + if line_height != self.line_height { + self.line_height = line_height; + self.hover_state_changed(cx); + } - if let Some(layer) = &self.layer { + 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(); - return MouseEventHandler::::new(0, cx, move |_, cx| { + 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| { + move |this, range, items, _| { let mut cursor = layer.node().walk(); let mut descendant_ix = range.start as usize; cursor.goto_descendant(descendant_ix); @@ -243,19 +305,19 @@ impl View for SyntaxTreeView { } } else { let node = cursor.node(); - let is_hovered = Some(descendant_ix) == this.active_descendant_ix; - if is_hovered { - this.node_is_active(node, cx); - } + let hovered = Some(descendant_ix) == this.hovered_descendant_ix; + let selected = Some(descendant_ix) == this.selected_descendant_ix; items.push( Label::new(node.kind(), style.clone()) .contained() - .with_background_color(if is_hovered { + .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(depth as f32 * 10.0) + .with_padding_left(depth as f32 * 18.0) .into_any(), ); descendant_ix += 1; @@ -271,13 +333,15 @@ impl View for SyntaxTreeView { }) .on_move(move |event, this, cx| { let y = event.position.y() - event.region.origin_y(); - this.hover_y = Some(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(); } From 56b749788f38686bf28bc57aa7af858ac02c7425 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 12 Jun 2023 12:18:01 -0700 Subject: [PATCH 3/6] Improve the look of the syntax tree view UI --- crates/language/src/buffer.rs | 4 + crates/language_tools/src/language_tools.rs | 2 +- crates/language_tools/src/lsp_log.rs | 32 +- crates/language_tools/src/syntax_tree_view.rs | 375 ++++++++++++++++-- crates/theme/src/theme.rs | 21 +- crates/zed/src/zed.rs | 3 + styles/src/styleTree/app.ts | 4 +- .../{lspLogMenu.ts => toolbarDropdownMenu.ts} | 8 +- 8 files changed, 382 insertions(+), 67 deletions(-) rename styles/src/styleTree/{lspLogMenu.ts => toolbarDropdownMenu.ts} (81%) diff --git a/crates/language/src/buffer.rs b/crates/language/src/buffer.rs index d9efb0fbc674ec84ab352f4d8922cd1e557399e7..5041ab759d1fe0aa892feec2b472086f62a01242 100644 --- a/crates/language/src/buffer.rs +++ b/crates/language/src/buffer.rs @@ -2117,6 +2117,10 @@ impl BufferSnapshot { } } + pub fn syntax_layers(&self) -> impl Iterator + '_ { + self.syntax.layers_for_range(0..self.len(), &self.text) + } + pub fn syntax_layer_at(&self, position: D) -> Option { let offset = position.to_offset(self); self.syntax diff --git a/crates/language_tools/src/language_tools.rs b/crates/language_tools/src/language_tools.rs index b61c8d3c80a0c9b34fe213288d2b6dba80bb2391..0a1f31f03fe82eece9911a9ecc474cd714a364c4 100644 --- a/crates/language_tools/src/language_tools.rs +++ b/crates/language_tools/src/language_tools.rs @@ -7,7 +7,7 @@ mod lsp_log_tests; use gpui::AppContext; pub use lsp_log::{LogStore, LspLogToolbarItemView, LspLogView}; -pub use syntax_tree_view::SyntaxTreeView; +pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView}; pub fn init(cx: &mut AppContext) { lsp_log::init(cx); diff --git a/crates/language_tools/src/lsp_log.rs b/crates/language_tools/src/lsp_log.rs index cec690d4a6fd09cffbc701196e9752672370fab4..b9036fa6d2a0cc9468bc816093d1526a652161fd 100644 --- a/crates/language_tools/src/lsp_log.rs +++ b/crates/language_tools/src/lsp_log.rs @@ -541,12 +541,7 @@ impl View for LspLogToolbarItemView { let theme = theme::current(cx).clone(); let Some(log_view) = self.log_view.as_ref() else { return Empty::new().into_any() }; let log_view = log_view.read(cx); - - let menu_rows = self - .log_view - .as_ref() - .and_then(|view| view.read(cx).menu_items(cx)) - .unwrap_or_default(); + let menu_rows = log_view.menu_items(cx).unwrap_or_default(); let current_server_id = log_view.current_server_id; let current_server = current_server_id.and_then(|current_server_id| { @@ -583,7 +578,7 @@ impl View for LspLogToolbarItemView { ) })) .contained() - .with_style(theme.lsp_log_menu.container) + .with_style(theme.toolbar_dropdown_menu.container) .constrained() .with_width(400.) .with_height(400.) @@ -593,6 +588,7 @@ impl View for LspLogToolbarItemView { cx.notify() }), ) + .with_hoverable(true) .with_fit_mode(OverlayFitMode::SwitchAnchor) .with_anchor_corner(AnchorCorner::TopLeft) .with_z_index(999) @@ -685,7 +681,7 @@ impl LspLogToolbarItemView { ) }) .unwrap_or_else(|| "No server selected".into()); - let style = theme.lsp_log_menu.header.style_for(state, false); + let style = theme.toolbar_dropdown_menu.header.style_for(state, false); Label::new(label, style.text.clone()) .contained() .with_style(style.container) @@ -711,7 +707,7 @@ impl LspLogToolbarItemView { Flex::column() .with_child({ - let style = &theme.lsp_log_menu.server; + let style = &theme.toolbar_dropdown_menu.section_header; Label::new( format!("{} ({})", name.0, worktree.read(cx).root_name()), style.text.clone(), @@ -719,16 +715,19 @@ impl LspLogToolbarItemView { .contained() .with_style(style.container) .constrained() - .with_height(theme.lsp_log_menu.row_height) + .with_height(theme.toolbar_dropdown_menu.row_height) }) .with_child( MouseEventHandler::::new(id.0, cx, move |state, _| { - let style = theme.lsp_log_menu.item.style_for(state, logs_selected); + let style = theme + .toolbar_dropdown_menu + .item + .style_for(state, logs_selected); Label::new(SERVER_LOGS, style.text.clone()) .contained() .with_style(style.container) .constrained() - .with_height(theme.lsp_log_menu.row_height) + .with_height(theme.toolbar_dropdown_menu.row_height) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { @@ -737,12 +736,15 @@ impl LspLogToolbarItemView { ) .with_child( MouseEventHandler::::new(id.0, cx, move |state, cx| { - let style = theme.lsp_log_menu.item.style_for(state, rpc_trace_selected); + let style = theme + .toolbar_dropdown_menu + .item + .style_for(state, rpc_trace_selected); Flex::row() .with_child( Label::new(RPC_MESSAGES, style.text.clone()) .constrained() - .with_height(theme.lsp_log_menu.row_height), + .with_height(theme.toolbar_dropdown_menu.row_height), ) .with_child( ui::checkbox_with_label::( @@ -761,7 +763,7 @@ impl LspLogToolbarItemView { .contained() .with_style(style.container) .constrained() - .with_height(theme.lsp_log_menu.row_height) + .with_height(theme.toolbar_dropdown_menu.row_height) }) .with_cursor_style(CursorStyle::PointingHand) .on_click(MouseButton::Left, move |_, view, cx| { diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 781961af07b08ce636ccc472e6970cf724a01daa..3f9b087f700c50b66a5c3108ed5dc58f432a4cfc 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -1,17 +1,21 @@ use editor::{scroll::autoscroll::Autoscroll, Anchor, Editor, ExcerptId}; use gpui::{ actions, - elements::{Empty, Label, MouseEventHandler, ScrollTarget, UniformList, UniformListState}, + elements::{ + AnchorCorner, Empty, Flex, Label, MouseEventHandler, Overlay, OverlayFitMode, + ParentElement, ScrollTarget, Stack, UniformList, UniformListState, + }, fonts::TextStyle, - platform::MouseButton, - AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, + platform::{CursorStyle, MouseButton}, + AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle, }; -use language::{Buffer, OwnedSyntaxLayerInfo}; -use std::ops::Range; -use theme::ThemeSettings; +use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; +use std::{ops::Range, sync::Arc}; +use theme::{Theme, ThemeSettings}; +use tree_sitter::Node; use workspace::{ item::{Item, ItemHandle}, - Workspace, + ToolbarItemLocation, ToolbarItemView, Workspace, }; actions!(log, [OpenSyntaxTreeView]); @@ -19,13 +23,17 @@ actions!(log, [OpenSyntaxTreeView]); pub fn init(cx: &mut AppContext) { cx.add_action( move |workspace: &mut Workspace, _: &OpenSyntaxTreeView, cx: _| { - let syntax_tree_view = cx.add_view(|cx| SyntaxTreeView::new(workspace, cx)); + let active_item = workspace.active_item(cx); + let workspace_handle = workspace.weak_handle(); + let syntax_tree_view = + cx.add_view(|cx| SyntaxTreeView::new(workspace_handle, active_item, cx)); workspace.add_item(Box::new(syntax_tree_view), cx); }, ); } pub struct SyntaxTreeView { + workspace_handle: WeakViewHandle, editor: Option, mouse_y: Option, line_height: Option, @@ -34,12 +42,19 @@ pub struct SyntaxTreeView { 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: ModelHandle, excerpt_id: ExcerptId, @@ -47,8 +62,13 @@ struct BufferState { } impl SyntaxTreeView { - pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + pub fn new( + workspace_handle: WeakViewHandle, + 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, @@ -57,9 +77,9 @@ impl SyntaxTreeView { selected_descendant_ix: None, }; - this.workspace_updated(workspace.active_item(cx), cx); + this.workspace_updated(active_item, cx); cx.observe( - &workspace.weak_handle().upgrade(cx).unwrap(), + &workspace_handle.upgrade(cx).unwrap(), |this, workspace, cx| { this.workspace_updated(workspace.read(cx).active_item(cx), cx); }, @@ -94,12 +114,12 @@ impl SyntaxTreeView { } let subscription = cx.subscribe(&editor, |this, _, event, cx| { - let reset_layer = match event { + let did_reparse = match event { editor::Event::Reparsed => true, editor::Event::SelectionsChanged { .. } => false, _ => return, }; - this.editor_updated(reset_layer, cx); + this.editor_updated(did_reparse, cx); }); self.editor = Some(EditorState { @@ -110,7 +130,7 @@ impl SyntaxTreeView { self.editor_updated(true, cx); } - fn editor_updated(&mut self, reset_layer: bool, cx: &mut ViewContext) -> Option<()> { + 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); @@ -129,24 +149,39 @@ impl SyntaxTreeView { excerpt_id, active_layer: None, }); - if reset_layer - || buffer_state.buffer != buffer - || buffer_state.excerpt_id != buffer_state.excerpt_id - { + 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; } - // Within the active layer, find the syntax node under the cursor, - // and scroll to it. let layer = match &mut buffer_state.active_layer { Some(layer) => layer, None => { - let layer = buffer.read(cx).snapshot().syntax_layer_at(0)?.to_owned(); - buffer_state.active_layer.insert(layer) + 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 { @@ -236,6 +271,55 @@ impl SyntaxTreeView { }); Some(()) } + + fn render_node( + node: Node, + depth: u32, + selected: bool, + hovered: bool, + list_hovered: bool, + style: &TextStyle, + editor_theme: &theme::Editor, + cx: &AppContext, + ) -> gpui::AnyElement { + let mut range_style = style.clone(); + let mut anonymous_node_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 string_color = editor_theme + .syntax + .highlights + .iter() + .find_map(|(name, style)| (name == "string").then(|| style.color)?); + if let Some(color) = string_color { + anonymous_node_style.color = color; + } + + Flex::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 { @@ -269,9 +353,9 @@ impl View for SyntaxTreeView { underline: Default::default(), }; - let line_height = Some(cx.font_cache().line_height(font_size)); - if line_height != self.line_height { - self.line_height = line_height; + 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); } @@ -282,13 +366,14 @@ impl View for SyntaxTreeView { .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, _| { + move |this, range, items, cx| { let mut cursor = layer.node().walk(); let mut descendant_ix = range.start as usize; cursor.goto_descendant(descendant_ix); @@ -304,22 +389,16 @@ impl View for SyntaxTreeView { break; } } else { - let node = cursor.node(); - let hovered = Some(descendant_ix) == this.hovered_descendant_ix; - let selected = Some(descendant_ix) == this.selected_descendant_ix; - items.push( - Label::new(node.kind(), style.clone()) - .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(depth as f32 * 18.0) - .into_any(), - ); + items.push(Self::render_node( + cursor.node(), + 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; @@ -358,4 +437,216 @@ impl Item for SyntaxTreeView { ) -> 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; + 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, false); + 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 + .style_for(state, is_selected); + 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 + } } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 9bd17910d2a166bbe56b2a6cfffcd404c686e1e9..c7563ec87a3dfd20f3f2682a2a6e9848cecf5279 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -44,7 +44,7 @@ pub struct Theme { pub context_menu: ContextMenu, pub contacts_popover: ContactsPopover, pub contact_list: ContactList, - pub lsp_log_menu: LspLogMenu, + pub toolbar_dropdown_menu: DropdownMenu, pub copilot: Copilot, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, @@ -246,15 +246,26 @@ pub struct ContactFinder { } #[derive(Deserialize, Default)] -pub struct LspLogMenu { +pub struct DropdownMenu { #[serde(flatten)] pub container: ContainerStyle, - pub header: Interactive, - pub server: ContainedText, - pub item: Interactive, + pub header: Interactive, + pub section_header: ContainedText, + pub item: Interactive, pub row_height: f32, } +#[derive(Deserialize, Default)] +pub struct DropdownMenuItem { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub text: TextStyle, + pub secondary_text: Option, + #[serde(default)] + pub secondary_text_spacing: f32, +} + #[derive(Clone, Deserialize, Default)] pub struct TabBar { #[serde(flatten)] diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index cef6cc7d3c35a0d88e6f804ea196c03fa0c01c06..b0b88b37ace7da3d161d87fb1b4367dfae9e8a76 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -314,6 +314,9 @@ pub fn initialize_workspace( 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); }) }); } diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 886553d418d08095c80d5ebfceef9502e7ab9bd0..6244cbae102b2a0d44d0494438182b07c1144f4a 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -17,7 +17,7 @@ import projectSharedNotification from "./projectSharedNotification" import tooltip from "./tooltip" import terminal from "./terminal" import contactList from "./contactList" -import lspLogMenu from "./lspLogMenu" +import toolbarDropdownMenu from "./toolbarDropdownMenu" import incomingCallNotification from "./incomingCallNotification" import { ColorScheme } from "../theme/colorScheme" import feedback from "./feedback" @@ -46,7 +46,7 @@ export default function app(colorScheme: ColorScheme): Object { contactsPopover: contactsPopover(colorScheme), contactFinder: contactFinder(colorScheme), contactList: contactList(colorScheme), - lspLogMenu: lspLogMenu(colorScheme), + toolbarDropdownMenu: toolbarDropdownMenu(colorScheme), search: search(colorScheme), sharedScreen: sharedScreen(colorScheme), updateNotification: updateNotification(colorScheme), diff --git a/styles/src/styleTree/lspLogMenu.ts b/styles/src/styleTree/toolbarDropdownMenu.ts similarity index 81% rename from styles/src/styleTree/lspLogMenu.ts rename to styles/src/styleTree/toolbarDropdownMenu.ts index 94dd4831b259facbe01ff2bf5136202b8c30faad..92616eb0223781deae3162bfd7d429a3f896e6eb 100644 --- a/styles/src/styleTree/lspLogMenu.ts +++ b/styles/src/styleTree/toolbarDropdownMenu.ts @@ -1,7 +1,7 @@ import { ColorScheme } from "../theme/colorScheme" import { background, border, text } from "./components" -export default function contactsPanel(colorScheme: ColorScheme) { +export default function dropdownMenu(colorScheme: ColorScheme) { let layer = colorScheme.middle return { @@ -11,6 +11,8 @@ export default function contactsPanel(colorScheme: ColorScheme) { shadow: colorScheme.popoverShadow, header: { ...text(layer, "sans", { size: "sm" }), + secondaryText: text(layer, "sans", { size: "sm", color: "#aaaaaa" }), + secondaryTextSpacing: 10, padding: { left: 8, right: 8, top: 2, bottom: 2 }, cornerRadius: 6, background: background(layer, "on"), @@ -20,12 +22,14 @@ export default function contactsPanel(colorScheme: ColorScheme) { ...text(layer, "sans", "hovered", { size: "sm" }), } }, - server: { + sectionHeader: { ...text(layer, "sans", { size: "sm" }), padding: { left: 8, right: 8, top: 8, bottom: 8 }, }, item: { ...text(layer, "sans", { size: "sm" }), + secondaryTextSpacing: 10, + secondaryText: text(layer, "sans", { size: "sm" }), padding: { left: 18, right: 18, top: 2, bottom: 2 }, hover: { background: background(layer, "hovered"), From b4f3a88b38d756f46916df24474c18f01805ab10 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 12 Jun 2023 15:14:34 -0700 Subject: [PATCH 4/6] Close the menu when selecting a different layer in the syntax tree view --- crates/language_tools/src/syntax_tree_view.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index 3f9b087f700c50b66a5c3108ed5dc58f432a4cfc..d63f274d9d49064da4cfa365a0386d28918761c6 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -530,6 +530,7 @@ 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(()) }) From 594b9def204940bcd66a5445f8aca7d02bcf1336 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 12 Jun 2023 15:14:39 -0700 Subject: [PATCH 5/6] Upgrade Tree-sitter --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 711c97df409334919357eca937fa3643c4d1c338..df97ab9c51338f444418c0858d6f65717535bb51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7360,7 +7360,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.10" -source = "git+https://github.com/tree-sitter/tree-sitter?rev=a2119cb6914d62e626fcc40684ef900d7fa90d86#a2119cb6914d62e626fcc40684ef900d7fa90d86" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=49226023693107fba9a1191136a4f47f38cdca73#49226023693107fba9a1191136a4f47f38cdca73" dependencies = [ "cc", "regex", @@ -7560,7 +7560,7 @@ dependencies = [ [[package]] name = "tree-sitter-yaml" version = "0.0.1" -source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=5694b7f290cd9ef998829a0a6d8391a666370886#5694b7f290cd9ef998829a0a6d8391a666370886" +source = "git+https://github.com/zed-industries/tree-sitter-yaml?rev=f545a41f57502e1b5ddf2a6668896c1b0620f930#f545a41f57502e1b5ddf2a6668896c1b0620f930" dependencies = [ "cc", "tree-sitter", diff --git a/Cargo.toml b/Cargo.toml index e8003684c3179cf35c768bbb210bb5d4ff28cc2f..fca735596489a222208e2f65874efc4fcd04ffd5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,7 +102,7 @@ tree-sitter = "0.20" unindent = { version = "0.1.7" } [patch.crates-io] -tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "a2119cb6914d62e626fcc40684ef900d7fa90d86" } +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "49226023693107fba9a1191136a4f47f38cdca73" } async-task = { git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e" } # TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457 From 018466171bfd44055479192bf9c6678d00351f01 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 12 Jun 2023 15:32:16 -0700 Subject: [PATCH 6/6] Include field names in the syntax tree --- crates/language_tools/src/syntax_tree_view.rs | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/crates/language_tools/src/syntax_tree_view.rs b/crates/language_tools/src/syntax_tree_view.rs index d63f274d9d49064da4cfa365a0386d28918761c6..6a939a1543c026ac1902405c8a11295c380f836f 100644 --- a/crates/language_tools/src/syntax_tree_view.rs +++ b/crates/language_tools/src/syntax_tree_view.rs @@ -10,9 +10,9 @@ use gpui::{ AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle, }; use language::{Buffer, OwnedSyntaxLayerInfo, SyntaxLayerInfo}; -use std::{ops::Range, sync::Arc}; +use std::{mem, ops::Range, sync::Arc}; use theme::{Theme, ThemeSettings}; -use tree_sitter::Node; +use tree_sitter::{Node, TreeCursor}; use workspace::{ item::{Item, ItemHandle}, ToolbarItemLocation, ToolbarItemView, Workspace, @@ -212,7 +212,10 @@ impl SyntaxTreeView { 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, range, cx| { + 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]); }); @@ -273,7 +276,7 @@ impl SyntaxTreeView { } fn render_node( - node: Node, + cursor: &TreeCursor, depth: u32, selected: bool, hovered: bool, @@ -282,23 +285,42 @@ impl SyntaxTreeView { editor_theme: &theme::Editor, cx: &AppContext, ) -> gpui::AnyElement { + let node = cursor.node(); let mut range_style = style.clone(); - let mut anonymous_node_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; } - Flex::row() + 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()) @@ -318,7 +340,7 @@ impl SyntaxTreeView { Default::default() }) .with_padding_left(gutter_padding + depth as f32 * 18.0) - .into_any() + .into_any(); } } @@ -390,7 +412,7 @@ impl View for SyntaxTreeView { } } else { items.push(Self::render_node( - cursor.node(), + &cursor, depth, Some(descendant_ix) == this.selected_descendant_ix, Some(descendant_ix) == this.hovered_descendant_ix,