mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-27 19:02:07 +00:00
Add a syntax tree view, for developing and debugging language support (#2601)
This PR adds a syntax tree view, which lets you view the syntax tree of any layer in the active editor's `SyntaxMap`. This view uses some new APIs that I added to Tree-sitter, which allow us to efficiently render the syntax tree using a `UniformList`. Tree-sitter PR: https://github.com/tree-sitter/tree-sitter/pull/2316 ![Screen Shot 2023-06-12 at 3 33 36 PM](https://github.com/zed-industries/zed/assets/326587/2a27ee7b-bf29-4b3b-bfa8-fb47f97a2785) Release Notes: - Added a *syntax tree view* that shows Zed's internal syntax tree(s) for the active editor. You can open it running the `debug: open syntax tree view` command from the command palette.
This commit is contained in:
commit
8542911eec
22 changed files with 877 additions and 126 deletions
53
Cargo.lock
generated
53
Cargo.lock
generated
|
@ -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=49226023693107fba9a1191136a4f47f38cdca73#49226023693107fba9a1191136a4f47f38cdca73"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
|
@ -7559,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",
|
||||
|
@ -8829,11 +8830,11 @@ dependencies = [
|
|||
"journal",
|
||||
"language",
|
||||
"language_selector",
|
||||
"language_tools",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"log",
|
||||
"lsp",
|
||||
"lsp_log",
|
||||
"node_runtime",
|
||||
"num_cpus",
|
||||
"outline",
|
||||
|
|
|
@ -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 = "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
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -7102,7 +7102,7 @@ impl Editor {
|
|||
|
||||
let mut new_selections_by_buffer = HashMap::default();
|
||||
for selection in editor.selections.all::<usize>(cx) {
|
||||
for (buffer, mut range) in
|
||||
for (buffer, mut range, _) in
|
||||
buffer.range_to_buffer_ranges(selection.start..selection.end, cx)
|
||||
{
|
||||
if selection.reversed {
|
||||
|
|
|
@ -1118,7 +1118,7 @@ impl MultiBuffer {
|
|||
&self,
|
||||
point: T,
|
||||
cx: &AppContext,
|
||||
) -> Option<(ModelHandle<Buffer>, usize)> {
|
||||
) -> Option<(ModelHandle<Buffer>, usize, ExcerptId)> {
|
||||
let snapshot = self.read(cx);
|
||||
let offset = point.to_offset(&snapshot);
|
||||
let mut cursor = snapshot.excerpts.cursor::<usize>();
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1140,7 +1140,7 @@ impl MultiBuffer {
|
|||
&self,
|
||||
range: Range<T>,
|
||||
cx: &AppContext,
|
||||
) -> Vec<(ModelHandle<Buffer>, Range<usize>)> {
|
||||
) -> Vec<(ModelHandle<Buffer>, Range<usize>, 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(&());
|
||||
}
|
||||
|
||||
|
@ -1387,7 +1387,7 @@ impl MultiBuffer {
|
|||
cx: &'a AppContext,
|
||||
) -> Option<Arc<Language>> {
|
||||
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();
|
||||
|
@ -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())
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,20 @@ impl BufferSnapshot {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn language_at<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||
pub fn syntax_layers(&self) -> impl Iterator<Item = SyntaxLayerInfo> + '_ {
|
||||
self.syntax.layers_for_range(0..self.len(), &self.text)
|
||||
}
|
||||
|
||||
pub fn syntax_layer_at<D: ToOffset>(&self, position: D) -> Option<SyntaxLayerInfo> {
|
||||
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<D: ToOffset>(&self, position: D) -> Option<&Arc<Language>> {
|
||||
self.syntax_layer_at(position)
|
||||
.map(|info| info.language)
|
||||
.or(self.language.as_ref())
|
||||
}
|
||||
|
@ -2140,7 +2149,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 +2197,7 @@ impl BufferSnapshot {
|
|||
let range = range.start.to_offset(self)..range.end.to_offset(self);
|
||||
let mut result: Option<Range<usize>> = 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.
|
||||
|
|
|
@ -2242,7 +2242,7 @@ fn get_tree_sexp(buffer: &ModelHandle<Buffer>, 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()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -125,8 +125,17 @@ impl SyntaxLayerContent {
|
|||
#[derive(Debug)]
|
||||
pub struct SyntaxLayerInfo<'a> {
|
||||
pub depth: usize,
|
||||
pub node: Node<'a>,
|
||||
pub language: &'a Arc<Language>,
|
||||
tree: &'a Tree,
|
||||
offset: (usize, tree_sitter::Point),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OwnedSyntaxLayerInfo {
|
||||
pub depth: usize,
|
||||
pub language: Arc<Language>,
|
||||
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<u32> {
|
||||
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<usize>)> = 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,
|
||||
|
|
|
@ -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"] }
|
15
crates/language_tools/src/language_tools.rs
Normal file
15
crates/language_tools/src/language_tools.rs
Normal file
|
@ -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);
|
||||
}
|
|
@ -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<WeakModelHandle<Project>, ProjectState>,
|
||||
io_tx: mpsc::UnboundedSender<(WeakModelHandle<Project>, LanguageServerId, bool, String)>,
|
||||
}
|
||||
|
@ -49,10 +46,10 @@ struct LanguageServerRpcState {
|
|||
}
|
||||
|
||||
pub struct LspLogView {
|
||||
pub(crate) editor: ViewHandle<Editor>,
|
||||
log_store: ModelHandle<LogStore>,
|
||||
current_server_id: Option<LanguageServerId>,
|
||||
is_showing_rpc_trace: bool,
|
||||
editor: ViewHandle<Editor>,
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
|
@ -68,13 +65,13 @@ enum MessageKind {
|
|||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
struct LogMenuItem {
|
||||
server_id: LanguageServerId,
|
||||
server_name: LanguageServerName,
|
||||
worktree: ModelHandle<Worktree>,
|
||||
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<Worktree>,
|
||||
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>) -> Self {
|
||||
pub fn new(cx: &mut ModelContext<Self>) -> 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<Project>,
|
||||
log_store: ModelHandle<LogStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
|
@ -360,7 +357,7 @@ impl LspLogView {
|
|||
editor
|
||||
}
|
||||
|
||||
fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
|
||||
pub(crate) fn menu_items<'a>(&'a self, cx: &'a AppContext) -> Option<Vec<LogMenuItem>> {
|
||||
let log_store = self.log_store.read(cx);
|
||||
let state = log_store.projects.get(&self.project.downgrade())?;
|
||||
let mut rows = self
|
||||
|
@ -544,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| {
|
||||
|
@ -586,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.)
|
||||
|
@ -596,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)
|
||||
|
@ -688,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)
|
||||
|
@ -714,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(),
|
||||
|
@ -722,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::<ActivateLog, _>::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| {
|
||||
|
@ -740,12 +736,15 @@ impl LspLogToolbarItemView {
|
|||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<ActivateRpcTrace, _>::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::<Self, _, Self, _>(
|
||||
|
@ -764,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| {
|
|
@ -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]
|
675
crates/language_tools/src/syntax_tree_view.rs
Normal file
675
crates/language_tools/src/syntax_tree_view.rs
Normal file
|
@ -0,0 +1,675 @@
|
|||
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,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Element, Entity, ModelHandle, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
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!(log, [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.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<Workspace>,
|
||||
editor: Option<EditorState>,
|
||||
mouse_y: Option<f32>,
|
||||
line_height: Option<f32>,
|
||||
list_state: UniformListState,
|
||||
selected_descendant_ix: Option<usize>,
|
||||
hovered_descendant_ix: Option<usize>,
|
||||
}
|
||||
|
||||
pub struct SyntaxTreeToolbarItemView {
|
||||
tree_view: Option<ViewHandle<SyntaxTreeView>>,
|
||||
subscription: Option<gpui::Subscription>,
|
||||
menu_open: bool,
|
||||
}
|
||||
|
||||
struct EditorState {
|
||||
editor: ViewHandle<Editor>,
|
||||
active_buffer: Option<BufferState>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BufferState {
|
||||
buffer: ModelHandle<Buffer>,
|
||||
excerpt_id: ExcerptId,
|
||||
active_layer: Option<OwnedSyntaxLayerInfo>,
|
||||
}
|
||||
|
||||
impl SyntaxTreeView {
|
||||
pub fn new(
|
||||
workspace_handle: WeakViewHandle<Workspace>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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<Box<dyn ItemHandle>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let Some(item) = active_item {
|
||||
if item.id() != cx.view_id() {
|
||||
if let Some(editor) = item.act_as::<Editor>(cx) {
|
||||
self.set_editor(editor, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_editor(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(state) = &self.editor {
|
||||
if state.editor == editor {
|
||||
return;
|
||||
}
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights::<Self>(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<Self>) -> 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::<usize>(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<SyntaxTreeView>) -> 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<SyntaxTreeView>) {
|
||||
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::<Self>(cx);
|
||||
editor.highlight_background::<Self>(
|
||||
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<Self>,
|
||||
mut f: impl FnMut(&mut Editor, Range<Anchor>, &mut ViewContext<Editor>),
|
||||
) -> 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<SyntaxTreeView> {
|
||||
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<Self> {
|
||||
let settings = settings::get::<ThemeSettings>(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(),
|
||||
};
|
||||
|
||||
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::<Self, Self>::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<V: View>(
|
||||
&self,
|
||||
_: Option<usize>,
|
||||
style: &theme::Tab,
|
||||
_: &AppContext,
|
||||
) -> gpui::AnyElement<V> {
|
||||
Label::new("Syntax Tree", style.label.clone()).into_any()
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Self>
|
||||
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<gpui::AnyElement<Self>> {
|
||||
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::<Menu, _>::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>) {
|
||||
self.menu_open = !self.menu_open;
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn select_layer(&mut self, layer_ix: usize, cx: &mut ViewContext<Self>) -> 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<Theme>,
|
||||
active_layer: &OwnedSyntaxLayerInfo,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ToggleMenu {}
|
||||
MouseEventHandler::<ToggleMenu, Self>::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<Theme>,
|
||||
active_layer: &OwnedSyntaxLayerInfo,
|
||||
layer: SyntaxLayerInfo,
|
||||
layer_ix: usize,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> impl Element<Self> {
|
||||
enum ActivateLayer {}
|
||||
MouseEventHandler::<ActivateLayer, _>::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> {
|
||||
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<Self>,
|
||||
) -> workspace::ToolbarItemLocation {
|
||||
self.menu_open = false;
|
||||
if let Some(item) = active_pane_item {
|
||||
if let Some(view) = item.downcast::<SyntaxTreeView>() {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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]
|
||||
|
|
|
@ -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<ContainedText>,
|
||||
pub server: ContainedText,
|
||||
pub item: Interactive<ContainedText>,
|
||||
pub header: Interactive<DropdownMenuItem>,
|
||||
pub section_header: ContainedText,
|
||||
pub item: Interactive<DropdownMenuItem>,
|
||||
pub row_height: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct DropdownMenuItem {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub text: TextStyle,
|
||||
pub secondary_text: Option<TextStyle>,
|
||||
#[serde(default)]
|
||||
pub secondary_text_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize, Default)]
|
||||
pub struct TabBar {
|
||||
#[serde(flatten)]
|
||||
|
|
|
@ -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" }
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -312,8 +312,11 @@ 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);
|
||||
let syntax_tree_item = cx
|
||||
.add_view(|_| language_tools::SyntaxTreeToolbarItemView::new());
|
||||
toolbar.add_item(syntax_tree_item, cx);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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"),
|
Loading…
Reference in a new issue