logiguard fork v3: full patch set on verified 8c74db0 tree
Includes prior-session patches (carry forward so the app compiles): - crates/gpui/build.rs: cross-compile manifest fix - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method - crates/gpui/src/window.rs: Window::activate_with_token public API - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix Plus the focus-serial fix: - serial.rs: SerialKind::KeyboardEnter - client.rs: store wl_keyboard.enter serial; latest_serial_of() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
48
crates/language_tools/Cargo.toml
Normal file
48
crates/language_tools/Cargo.toml
Normal file
@@ -0,0 +1,48 @@
|
||||
[package]
|
||||
name = "language_tools"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/language_tools.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
client.workspace = true
|
||||
collections.workspace = true
|
||||
command_palette_hooks.workspace = true
|
||||
editor.workspace = true
|
||||
edit_prediction.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
lsp.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
sysinfo.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
release_channel.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
semver.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
theme_settings.workspace = true
|
||||
1
crates/language_tools/LICENSE-GPL
Symbolic link
1
crates/language_tools/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
1185
crates/language_tools/src/highlights_tree_view.rs
Normal file
1185
crates/language_tools/src/highlights_tree_view.rs
Normal file
File diff suppressed because it is too large
Load Diff
298
crates/language_tools/src/key_context_view.rs
Normal file
298
crates/language_tools/src/key_context_view.rs
Normal file
@@ -0,0 +1,298 @@
|
||||
use gpui::{
|
||||
Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription, Task,
|
||||
actions,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::json;
|
||||
use ui::{Button, ButtonStyle};
|
||||
use ui::{
|
||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||
LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, div,
|
||||
h_flex, px, v_flex,
|
||||
};
|
||||
use workspace::{Item, SplitDirection, Workspace};
|
||||
|
||||
actions!(
|
||||
dev,
|
||||
[
|
||||
/// Opens the key context view for debugging keybindings.
|
||||
OpenKeyContextView
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &OpenKeyContextView, window, cx| {
|
||||
let key_context_view = cx.new(|cx| KeyContextView::new(window, cx));
|
||||
workspace.split_item(
|
||||
SplitDirection::Right,
|
||||
Box::new(key_context_view),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
struct KeyContextView {
|
||||
pending_keystrokes: Option<Vec<Keystroke>>,
|
||||
last_keystrokes: Option<SharedString>,
|
||||
last_possibilities: Vec<(SharedString, SharedString, Option<bool>)>,
|
||||
context_stack: Vec<KeyContext>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: [Subscription; 2],
|
||||
}
|
||||
|
||||
impl KeyContextView {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let sub1 = cx.observe_keystrokes(|this, e, _, cx| {
|
||||
let mut pending = this.pending_keystrokes.take().unwrap_or_default();
|
||||
pending.push(e.keystroke.clone());
|
||||
let mut possibilities = cx.all_bindings_for_input(&pending);
|
||||
possibilities.reverse();
|
||||
this.last_keystrokes = Some(
|
||||
json!(pending.iter().map(|p| p.unparse()).join(" "))
|
||||
.to_string()
|
||||
.into(),
|
||||
);
|
||||
this.context_stack = e.context_stack.clone();
|
||||
this.last_possibilities = possibilities
|
||||
.into_iter()
|
||||
.map(|binding| {
|
||||
let match_state = if let Some(predicate) = binding.predicate() {
|
||||
if this.matches(&predicate) {
|
||||
if this.action_matches(&e.action, binding.action()) {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else if this.action_matches(&e.action, binding.action()) {
|
||||
Some(true)
|
||||
} else {
|
||||
Some(false)
|
||||
};
|
||||
let predicate = if let Some(predicate) = binding.predicate() {
|
||||
format!("{}", predicate)
|
||||
} else {
|
||||
"".to_string()
|
||||
};
|
||||
let mut name = binding.action().name();
|
||||
if name == "zed::NoAction" {
|
||||
name = "(null)"
|
||||
}
|
||||
|
||||
(
|
||||
name.to_owned().into(),
|
||||
json!(predicate).to_string().into(),
|
||||
match_state,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
cx.notify();
|
||||
});
|
||||
let sub2 = cx.observe_pending_input(window, |this, window, cx| {
|
||||
this.pending_keystrokes = window.pending_input_keystrokes().map(|k| k.to_vec());
|
||||
if this.pending_keystrokes.is_some() {
|
||||
this.last_keystrokes.take();
|
||||
}
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Self {
|
||||
context_stack: Vec::new(),
|
||||
pending_keystrokes: None,
|
||||
last_keystrokes: None,
|
||||
last_possibilities: Vec::new(),
|
||||
focus_handle: cx.focus_handle(),
|
||||
_subscriptions: [sub1, sub2],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for KeyContextView {}
|
||||
|
||||
impl Focusable for KeyContextView {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
impl KeyContextView {
|
||||
fn set_context_stack(&mut self, stack: Vec<KeyContext>, cx: &mut Context<Self>) {
|
||||
self.context_stack = stack;
|
||||
cx.notify()
|
||||
}
|
||||
|
||||
fn matches(&self, predicate: &KeyBindingContextPredicate) -> bool {
|
||||
predicate.depth_of(&self.context_stack).is_some()
|
||||
}
|
||||
|
||||
fn action_matches(&self, a: &Option<Box<dyn Action>>, b: &dyn Action) -> bool {
|
||||
if let Some(last_action) = a {
|
||||
last_action.partial_eq(b)
|
||||
} else {
|
||||
b.name() == "zed::NoAction"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for KeyContextView {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Keyboard Context".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn can_split(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| KeyContextView::new(window, cx))))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for KeyContextView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
use itertools::Itertools;
|
||||
|
||||
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||
v_flex()
|
||||
.id("key-context-view")
|
||||
.overflow_scroll()
|
||||
.size_full()
|
||||
.max_h_full()
|
||||
.pt_4()
|
||||
.pl_4()
|
||||
.track_focus(&self.focus_handle)
|
||||
.key_context("KeyContextView")
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Left,
|
||||
cx.listener(|this, _, window, cx| {
|
||||
this.last_keystrokes.take();
|
||||
this.set_context_stack(window.context_stack(), cx);
|
||||
}),
|
||||
)
|
||||
.on_mouse_up_out(
|
||||
MouseButton::Right,
|
||||
cx.listener(|_, _, window, cx| {
|
||||
cx.defer_in(window, |this, window, cx| {
|
||||
this.last_keystrokes.take();
|
||||
this.set_context_stack(window.context_stack(), cx);
|
||||
});
|
||||
}),
|
||||
)
|
||||
.child(Label::new("Keyboard Context").size(LabelSize::Large))
|
||||
.child(Label::new("This view lets you determine the current context stack for creating custom key bindings in Zed. When a keyboard shortcut is triggered, it also shows all the possible contexts it could have triggered in, and which one matched."))
|
||||
.child(
|
||||
h_flex()
|
||||
.mt_4()
|
||||
.gap_4()
|
||||
.child(
|
||||
Button::new("open_documentation", "Open Documentation")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
|
||||
)
|
||||
.child(
|
||||
Button::new("view_default_keymap", "View Default Keymap")
|
||||
.style(ButtonStyle::Filled)
|
||||
.key_binding(ui::KeyBinding::for_action(
|
||||
&zed_actions::OpenDefaultKeymap,
|
||||
cx
|
||||
))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
Button::new("edit_your_keymap", "Edit Keymap File")
|
||||
.style(ButtonStyle::Filled)
|
||||
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymapFile, cx))
|
||||
.on_click(|_, window, cx| {
|
||||
window.dispatch_action(zed_actions::OpenKeymapFile.boxed_clone(), cx);
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new("Current Context Stack")
|
||||
.size(LabelSize::Large)
|
||||
.mt_8(),
|
||||
)
|
||||
.children({
|
||||
self.context_stack.iter().enumerate().map(|(i, context)| {
|
||||
let primary = context.primary().map(|e| e.key.clone()).unwrap_or_default();
|
||||
let secondary = context
|
||||
.secondary()
|
||||
.map(|e| {
|
||||
if let Some(value) = e.value.as_ref() {
|
||||
format!("{}={}", e.key, value)
|
||||
} else {
|
||||
e.key.to_string()
|
||||
}
|
||||
})
|
||||
.join(" ");
|
||||
Label::new(format!("{} {}", primary, secondary)).ml(px(12. * (i + 1) as f32))
|
||||
})
|
||||
})
|
||||
.child(Label::new("Last Keystroke").mt_4().size(LabelSize::Large))
|
||||
.when_some(self.pending_keystrokes.as_ref(), |el, keystrokes| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"Waiting for more input: {}",
|
||||
keystrokes.iter().map(|k| k.unparse()).join(" ")
|
||||
))
|
||||
.ml(px(12.)),
|
||||
)
|
||||
})
|
||||
.when_some(self.last_keystrokes.as_ref(), |el, keystrokes| {
|
||||
el.child(Label::new(format!("Typed: {}", keystrokes)).ml_4())
|
||||
.children(
|
||||
self.last_possibilities
|
||||
.iter()
|
||||
.map(|(name, predicate, state)| {
|
||||
let (text, color) = match state {
|
||||
Some(true) => ("(match)", ui::Color::Success),
|
||||
Some(false) => ("(low precedence)", ui::Color::Hint),
|
||||
None => ("(no match)", ui::Color::Error),
|
||||
};
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.ml_8()
|
||||
.child(div().min_w(px(200.)).child(Label::new(name.clone())))
|
||||
.child(Label::new(predicate.clone()))
|
||||
.child(Label::new(text).color(color))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when_some(key_equivalents, |el, key_equivalents| {
|
||||
el.child(Label::new("Key Equivalents").mt_4().size(LabelSize::Large))
|
||||
.child(Label::new("Shortcuts defined using some characters have been remapped so that shortcuts can be typed without holding option."))
|
||||
.children(
|
||||
key_equivalents
|
||||
.iter()
|
||||
.sorted()
|
||||
.map(|(key, equivalent)| {
|
||||
Label::new(format!("cmd-{} => cmd-{}", key, equivalent)).ml_8()
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
57
crates/language_tools/src/language_tools.rs
Normal file
57
crates/language_tools/src/language_tools.rs
Normal file
@@ -0,0 +1,57 @@
|
||||
mod highlights_tree_view;
|
||||
mod key_context_view;
|
||||
pub mod lsp_button;
|
||||
pub mod lsp_log_view;
|
||||
mod syntax_tree_view;
|
||||
|
||||
#[cfg(test)]
|
||||
mod lsp_log_view_tests;
|
||||
|
||||
use gpui::{App, AppContext, Entity};
|
||||
|
||||
pub use highlights_tree_view::{HighlightsTreeToolbarItemView, HighlightsTreeView};
|
||||
pub use lsp_log_view::LspLogView;
|
||||
pub use syntax_tree_view::{SyntaxTreeToolbarItemView, SyntaxTreeView};
|
||||
use ui::{Context, Window};
|
||||
use workspace::{Item, ItemHandle, SplitDirection, Workspace};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
highlights_tree_view::init(cx);
|
||||
lsp_log_view::init(false, cx);
|
||||
syntax_tree_view::init(cx);
|
||||
key_context_view::init(cx);
|
||||
}
|
||||
|
||||
fn get_or_create_tool<T>(
|
||||
workspace: &mut Workspace,
|
||||
destination: SplitDirection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
new_tool: impl FnOnce(&mut Window, &mut Context<T>) -> T,
|
||||
) -> Entity<T>
|
||||
where
|
||||
T: Item,
|
||||
{
|
||||
if let Some(item) = workspace.item_of_type::<T>(cx) {
|
||||
return item;
|
||||
}
|
||||
|
||||
let new_tool = cx.new(|cx| new_tool(window, cx));
|
||||
match workspace.find_pane_in_direction(destination, cx) {
|
||||
Some(right_pane) => {
|
||||
workspace.add_item(
|
||||
right_pane,
|
||||
new_tool.boxed_clone(),
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
workspace.split_item(destination, new_tool.boxed_clone(), window, cx);
|
||||
}
|
||||
}
|
||||
new_tool
|
||||
}
|
||||
1343
crates/language_tools/src/lsp_button.rs
Normal file
1343
crates/language_tools/src/lsp_button.rs
Normal file
File diff suppressed because it is too large
Load Diff
1380
crates/language_tools/src/lsp_log_view.rs
Normal file
1380
crates/language_tools/src/lsp_log_view.rs
Normal file
File diff suppressed because it is too large
Load Diff
115
crates/language_tools/src/lsp_log_view_tests.rs
Normal file
115
crates/language_tools/src/lsp_log_view_tests.rs
Normal file
@@ -0,0 +1,115 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::lsp_log_view::LogMenuItem;
|
||||
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AppContext as _, TestAppContext, VisualTestContext};
|
||||
use language::{FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, tree_sitter_rust};
|
||||
use lsp::LanguageServerName;
|
||||
use project::{
|
||||
FakeFs, Project,
|
||||
lsp_store::log_store::{LanguageServerKind, LogKind, LogStore},
|
||||
};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_lsp_log_view(cx: &mut TestAppContext) {
|
||||
zlog::init_test();
|
||||
|
||||
init_test(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/the-root"),
|
||||
json!({
|
||||
"test.rs": "",
|
||||
"package.json": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
|
||||
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::LANGUAGE.into()),
|
||||
)));
|
||||
let mut fake_rust_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
name: "the-rust-language-server",
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let log_store = cx.new(|cx| LogStore::new(false, cx));
|
||||
log_store.update(cx, |store, cx| store.add_project(&project, cx));
|
||||
|
||||
let _rust_buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer_with_lsp(path!("/the-root/test.rs"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut language_server = fake_rust_servers.next().await.unwrap();
|
||||
language_server
|
||||
.receive_notification::<lsp::notification::DidOpenTextDocument>()
|
||||
.await;
|
||||
|
||||
let window =
|
||||
cx.add_window(|window, cx| LspLogView::new(project.clone(), log_store.clone(), window, cx));
|
||||
let log_view = window.root(cx).unwrap();
|
||||
let mut cx = VisualTestContext::from_window(*window, cx);
|
||||
|
||||
language_server.notify::<lsp::notification::LogMessage>(lsp::LogMessageParams {
|
||||
message: "hello from the server".into(),
|
||||
typ: lsp::MessageType::INFO,
|
||||
});
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
log_view.update(&mut cx, |view, cx| {
|
||||
assert_eq!(
|
||||
view.menu_items(cx).unwrap(),
|
||||
&[LogMenuItem {
|
||||
server_id: language_server.server.server_id(),
|
||||
server_name: LanguageServerName("the-rust-language-server".into()),
|
||||
worktree_root_name: project
|
||||
.read(cx)
|
||||
.worktrees(cx)
|
||||
.next()
|
||||
.unwrap()
|
||||
.read(cx)
|
||||
.root_name_str()
|
||||
.to_string(),
|
||||
rpc_trace_enabled: false,
|
||||
selected_entry: LogKind::Logs,
|
||||
trace_level: lsp::TraceValue::Off,
|
||||
server_kind: LanguageServerKind::Local {
|
||||
project: project.downgrade()
|
||||
}
|
||||
}]
|
||||
);
|
||||
assert_eq!(view.editor.read(cx).text(cx), "hello from the server\n");
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut gpui::TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
theme_settings::init(theme::LoadThemes::JustBase, cx);
|
||||
release_channel::init(semver::Version::new(0, 0, 0), cx);
|
||||
});
|
||||
}
|
||||
751
crates/language_tools/src/syntax_tree_view.rs
Normal file
751
crates/language_tools/src/syntax_tree_view.rs
Normal file
@@ -0,0 +1,751 @@
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use editor::{
|
||||
Anchor, Editor, HighlightKey, MultiBufferOffset, SelectionEffects, scroll::Autoscroll,
|
||||
};
|
||||
use gpui::{
|
||||
App, AppContext as _, Context, Div, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
Hsla, InteractiveElement, IntoElement, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
ParentElement, Render, ScrollStrategy, SharedString, Styled, Task, UniformListScrollHandle,
|
||||
WeakEntity, Window, actions, div, rems, uniform_list,
|
||||
};
|
||||
use language::{Buffer, OwnedSyntaxLayer};
|
||||
use std::{any::TypeId, mem, ops::Range};
|
||||
use theme::ActiveTheme;
|
||||
use tree_sitter::{Node, TreeCursor};
|
||||
use ui::{
|
||||
ButtonCommon, ButtonLike, Clickable, Color, ContextMenu, FluentBuilder as _, IconButton,
|
||||
IconName, Label, LabelCommon, LabelSize, PopoverMenu, StyledExt, Tooltip, WithScrollbar,
|
||||
h_flex, v_flex,
|
||||
};
|
||||
use workspace::{
|
||||
Event as WorkspaceEvent, SplitDirection, ToolbarItemEvent, ToolbarItemLocation,
|
||||
ToolbarItemView, Workspace,
|
||||
item::{Item, ItemHandle},
|
||||
};
|
||||
|
||||
actions!(
|
||||
dev,
|
||||
[
|
||||
/// Opens the syntax tree view for the current file.
|
||||
OpenSyntaxTreeView,
|
||||
]
|
||||
);
|
||||
|
||||
actions!(
|
||||
syntax_tree_view,
|
||||
[
|
||||
/// Update the syntax tree view to show the last focused file.
|
||||
UseActiveEditor
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
let syntax_tree_actions = [TypeId::of::<UseActiveEditor>()];
|
||||
|
||||
CommandPaletteFilter::update_global(cx, |this, _| {
|
||||
this.hide_action_types(&syntax_tree_actions);
|
||||
});
|
||||
|
||||
cx.observe_new(move |workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(move |workspace, _: &OpenSyntaxTreeView, window, cx| {
|
||||
CommandPaletteFilter::update_global(cx, |this, _| {
|
||||
this.show_action_types(&syntax_tree_actions);
|
||||
});
|
||||
|
||||
let active_item = workspace.active_item(cx);
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let syntax_tree_view = cx.new(|cx| {
|
||||
cx.on_release(move |view: &mut SyntaxTreeView, cx| {
|
||||
if view
|
||||
.workspace_handle
|
||||
.read_with(cx, |workspace, cx| {
|
||||
workspace.item_of_type::<SyntaxTreeView>(cx).is_none()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
{
|
||||
CommandPaletteFilter::update_global(cx, |this, _| {
|
||||
this.hide_action_types(&syntax_tree_actions);
|
||||
});
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
SyntaxTreeView::new(workspace_handle, active_item, window, cx)
|
||||
});
|
||||
workspace.split_item(
|
||||
SplitDirection::Right,
|
||||
Box::new(syntax_tree_view),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.register_action(|workspace, _: &UseActiveEditor, window, cx| {
|
||||
if let Some(tree_view) = workspace.item_of_type::<SyntaxTreeView>(cx) {
|
||||
tree_view.update(cx, |view, cx| {
|
||||
view.update_active_editor(&Default::default(), window, cx)
|
||||
})
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct SyntaxTreeView {
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
editor: Option<EditorState>,
|
||||
list_scroll_handle: UniformListScrollHandle,
|
||||
/// The last active editor in the workspace. Note that this is specifically not the
|
||||
/// currently shown editor.
|
||||
last_active_editor: Option<Entity<Editor>>,
|
||||
selected_descendant_ix: Option<usize>,
|
||||
hovered_descendant_ix: Option<usize>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
pub struct SyntaxTreeToolbarItemView {
|
||||
tree_view: Option<Entity<SyntaxTreeView>>,
|
||||
subscription: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
struct EditorState {
|
||||
editor: Entity<Editor>,
|
||||
active_buffer: Option<BufferState>,
|
||||
_subscription: gpui::Subscription,
|
||||
}
|
||||
|
||||
impl EditorState {
|
||||
fn has_language(&self) -> bool {
|
||||
self.active_buffer
|
||||
.as_ref()
|
||||
.is_some_and(|buffer| buffer.active_layer.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct BufferState {
|
||||
buffer: Entity<Buffer>,
|
||||
active_layer: Option<OwnedSyntaxLayer>,
|
||||
}
|
||||
|
||||
impl SyntaxTreeView {
|
||||
pub fn new(
|
||||
workspace_handle: WeakEntity<Workspace>,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mut this = Self {
|
||||
workspace_handle: workspace_handle.clone(),
|
||||
list_scroll_handle: UniformListScrollHandle::new(),
|
||||
editor: None,
|
||||
last_active_editor: None,
|
||||
hovered_descendant_ix: None,
|
||||
selected_descendant_ix: None,
|
||||
focus_handle: cx.focus_handle(),
|
||||
};
|
||||
|
||||
this.handle_item_updated(active_item, window, cx);
|
||||
|
||||
cx.subscribe_in(
|
||||
&workspace_handle.upgrade().unwrap(),
|
||||
window,
|
||||
move |this, workspace, event, window, cx| match event {
|
||||
WorkspaceEvent::ItemAdded { .. } | WorkspaceEvent::ActiveItemChanged => {
|
||||
this.handle_item_updated(workspace.read(cx).active_item(cx), window, cx)
|
||||
}
|
||||
WorkspaceEvent::ItemRemoved { item_id } => {
|
||||
this.handle_item_removed(item_id, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn handle_item_updated(
|
||||
&mut self,
|
||||
active_item: Option<Box<dyn ItemHandle>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(editor) = active_item
|
||||
.filter(|item| item.item_id() != cx.entity_id())
|
||||
.and_then(|item| item.act_as::<Editor>(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
if let Some(editor_state) = self.editor.as_ref().filter(|state| state.has_language()) {
|
||||
self.last_active_editor = (editor_state.editor != editor).then_some(editor);
|
||||
} else {
|
||||
self.set_editor(editor, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_item_removed(
|
||||
&mut self,
|
||||
item_id: &EntityId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self
|
||||
.editor
|
||||
.as_ref()
|
||||
.is_some_and(|state| state.editor.entity_id() == *item_id)
|
||||
{
|
||||
self.editor = None;
|
||||
// Try activating the last active editor if there is one
|
||||
self.update_active_editor(&Default::default(), window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
fn update_active_editor(
|
||||
&mut self,
|
||||
_: &UseActiveEditor,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(editor) = self.last_active_editor.take() else {
|
||||
return;
|
||||
};
|
||||
self.set_editor(editor, window, cx);
|
||||
}
|
||||
|
||||
fn set_editor(&mut self, editor: Entity<Editor>, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(state) = &self.editor {
|
||||
if state.editor == editor {
|
||||
return;
|
||||
}
|
||||
let key = HighlightKey::SyntaxTreeView(cx.entity_id().as_u64() as usize);
|
||||
editor.update(cx, |editor, cx| editor.clear_background_highlights(key, cx));
|
||||
}
|
||||
|
||||
let subscription = cx.subscribe_in(&editor, window, |this, _, event, window, cx| {
|
||||
let did_reparse = match event {
|
||||
editor::EditorEvent::Reparsed(_) => true,
|
||||
editor::EditorEvent::SelectionsChanged { .. } => false,
|
||||
_ => return,
|
||||
};
|
||||
this.editor_updated(did_reparse, window, cx);
|
||||
});
|
||||
|
||||
self.editor = Some(EditorState {
|
||||
editor,
|
||||
_subscription: subscription,
|
||||
active_buffer: None,
|
||||
});
|
||||
self.editor_updated(true, window, cx);
|
||||
}
|
||||
|
||||
fn editor_updated(
|
||||
&mut self,
|
||||
did_reparse: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<()> {
|
||||
// Find which excerpt the cursor is in, and the position within that excerpted buffer.
|
||||
let editor_state = self.editor.as_mut()?;
|
||||
let snapshot = editor_state
|
||||
.editor
|
||||
.update(cx, |editor, cx| editor.snapshot(window, cx));
|
||||
let (buffer, range) = editor_state.editor.update(cx, |editor, cx| {
|
||||
let selection_range = editor
|
||||
.selections
|
||||
.last::<MultiBufferOffset>(&editor.display_snapshot(cx))
|
||||
.range();
|
||||
let multi_buffer = editor.buffer().read(cx);
|
||||
let (buffer, range, _) = snapshot
|
||||
.buffer_snapshot()
|
||||
.range_to_buffer_ranges(selection_range.start..selection_range.end)
|
||||
.pop()?;
|
||||
let buffer = multi_buffer.buffer(buffer.remote_id()).unwrap();
|
||||
Some((buffer, range))
|
||||
})?;
|
||||
|
||||
// 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(),
|
||||
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.buffer = buffer.clone();
|
||||
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.0).is_some() {
|
||||
if !range.is_empty() && cursor.node().end_byte() == range.start.0 {
|
||||
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.0 && node_range.end >= range.end.0 {
|
||||
break;
|
||||
}
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let descendant_ix = cursor.descendant_index();
|
||||
self.selected_descendant_ix = Some(descendant_ix);
|
||||
self.list_scroll_handle
|
||||
.scroll_to_item(descendant_ix, ScrollStrategy::Center);
|
||||
|
||||
cx.notify();
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn update_editor_with_range_for_descendant_ix(
|
||||
&self,
|
||||
descendant_ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
f: &mut dyn FnMut(&mut Editor, Range<Anchor>, usize, &mut Window, &mut Context<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 range = multibuffer.buffer_anchor_range_to_anchor_range(range)?;
|
||||
let key = cx.entity_id().as_u64() as usize;
|
||||
|
||||
// Update the editor with the anchor range.
|
||||
editor_state.editor.update(cx, |editor, cx| {
|
||||
f(editor, range, key, window, cx);
|
||||
});
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn render_node(cursor: &TreeCursor, depth: u32, selected: bool, cx: &App) -> Div {
|
||||
let colors = cx.theme().colors();
|
||||
let mut row = h_flex();
|
||||
if let Some(field_name) = cursor.field_name() {
|
||||
row = row.children([Label::new(field_name).color(Color::Info), Label::new(": ")]);
|
||||
}
|
||||
|
||||
let node = cursor.node();
|
||||
row.child(if node.is_named() {
|
||||
Label::new(node.kind()).color(Color::Default)
|
||||
} else {
|
||||
Label::new(format!("\"{}\"", node.kind())).color(Color::Created)
|
||||
})
|
||||
.child(
|
||||
div()
|
||||
.child(Label::new(format_node_range(node)).color(Color::Muted))
|
||||
.pl_1(),
|
||||
)
|
||||
.text_bg(if selected {
|
||||
colors.element_selected
|
||||
} else {
|
||||
Hsla::default()
|
||||
})
|
||||
.pl(rems(depth as f32))
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
}
|
||||
|
||||
fn compute_items(
|
||||
&mut self,
|
||||
layer: &OwnedSyntaxLayer,
|
||||
range: Range<usize>,
|
||||
cx: &Context<Self>,
|
||||
) -> Vec<Div> {
|
||||
let mut items = Vec::new();
|
||||
let mut cursor = layer.node().walk();
|
||||
let mut descendant_ix = range.start;
|
||||
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) == self.selected_descendant_ix,
|
||||
cx,
|
||||
)
|
||||
.on_mouse_down(
|
||||
MouseButton::Left,
|
||||
cx.listener(move |tree_view, _: &MouseDownEvent, window, cx| {
|
||||
tree_view.update_editor_with_range_for_descendant_ix(
|
||||
descendant_ix,
|
||||
window,
|
||||
cx,
|
||||
&mut |editor, mut range, _, window, cx| {
|
||||
// Put the cursor at the beginning of the node.
|
||||
mem::swap(&mut range.start, &mut range.end);
|
||||
|
||||
editor.change_selections(
|
||||
SelectionEffects::scroll(Autoscroll::newest()),
|
||||
window,
|
||||
cx,
|
||||
|selections| {
|
||||
selections.select_ranges([range]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
)
|
||||
.on_mouse_move(cx.listener(
|
||||
move |tree_view, _: &MouseMoveEvent, window, cx| {
|
||||
if tree_view.hovered_descendant_ix != Some(descendant_ix) {
|
||||
tree_view.hovered_descendant_ix = Some(descendant_ix);
|
||||
tree_view.update_editor_with_range_for_descendant_ix(
|
||||
descendant_ix,
|
||||
window,
|
||||
cx,
|
||||
&mut |editor, range, key, _, cx| {
|
||||
Self::set_editor_highlights(editor, key, &[range], cx);
|
||||
},
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
)),
|
||||
);
|
||||
descendant_ix += 1;
|
||||
if cursor.goto_first_child() {
|
||||
depth += 1;
|
||||
} else {
|
||||
visited_children = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
items
|
||||
}
|
||||
|
||||
fn set_editor_highlights(
|
||||
editor: &mut Editor,
|
||||
key: usize,
|
||||
ranges: &[Range<Anchor>],
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
editor.highlight_background(
|
||||
HighlightKey::SyntaxTreeView(key),
|
||||
ranges,
|
||||
|_, theme| theme.colors().editor_document_highlight_write_background,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
fn clear_editor_highlights(editor: &Entity<Editor>, cx: &mut Context<Self>) {
|
||||
let highlight_key = HighlightKey::SyntaxTreeView(cx.entity_id().as_u64() as usize);
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.clear_background_highlights(highlight_key, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SyntaxTreeView {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.map(|this| {
|
||||
let editor_state = self.editor.as_ref();
|
||||
|
||||
if let Some(layer) = editor_state
|
||||
.and_then(|editor| editor.active_buffer.as_ref())
|
||||
.and_then(|buffer| buffer.active_layer.as_ref())
|
||||
{
|
||||
let layer = layer.clone();
|
||||
this.child(
|
||||
uniform_list(
|
||||
"SyntaxTreeView",
|
||||
layer.node().descendant_count(),
|
||||
cx.processor(move |this, range: Range<usize>, _, cx| {
|
||||
this.compute_items(&layer, range, cx)
|
||||
}),
|
||||
)
|
||||
.size_full()
|
||||
.track_scroll(&self.list_scroll_handle)
|
||||
.text_bg(cx.theme().colors().background)
|
||||
.into_any_element(),
|
||||
)
|
||||
.vertical_scrollbar_for(&self.list_scroll_handle, window, cx)
|
||||
.into_any_element()
|
||||
} else {
|
||||
let inner_content = v_flex()
|
||||
.items_center()
|
||||
.text_center()
|
||||
.gap_2()
|
||||
.max_w_3_5()
|
||||
.map(|this| {
|
||||
if editor_state.is_some_and(|state| !state.has_language()) {
|
||||
this.child(Label::new("Current editor has no associated language"))
|
||||
.child(
|
||||
Label::new(concat!(
|
||||
"Try assigning a language or",
|
||||
"switching to a different buffer"
|
||||
))
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
} else {
|
||||
this.child(Label::new("Not attached to an editor")).child(
|
||||
Label::new("Focus an editor to show a new tree view")
|
||||
.size(LabelSize::Small),
|
||||
)
|
||||
}
|
||||
});
|
||||
|
||||
this.h_flex()
|
||||
.size_full()
|
||||
.justify_center()
|
||||
.child(inner_content)
|
||||
.into_any_element()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for SyntaxTreeView {}
|
||||
|
||||
impl Focusable for SyntaxTreeView {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for SyntaxTreeView {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: &mut dyn FnMut(workspace::item::ItemEvent)) {}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Syntax Tree".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn can_split(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Task::ready(Some(cx.new(|cx| {
|
||||
let mut clone = Self::new(self.workspace_handle.clone(), None, window, cx);
|
||||
if let Some(editor) = &self.editor {
|
||||
clone.set_editor(editor.editor.clone(), window, cx)
|
||||
}
|
||||
clone
|
||||
})))
|
||||
}
|
||||
|
||||
fn on_removed(&self, cx: &mut Context<Self>) {
|
||||
if let Some(state) = self.editor.as_ref() {
|
||||
Self::clear_editor_highlights(&state.editor, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SyntaxTreeToolbarItemView {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl SyntaxTreeToolbarItemView {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tree_view: None,
|
||||
subscription: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_menu(&mut self, cx: &mut Context<Self>) -> Option<PopoverMenu<ContextMenu>> {
|
||||
let tree_view = self.tree_view.as_ref()?;
|
||||
let tree_view = tree_view.read(cx);
|
||||
|
||||
let editor_state = tree_view.editor.as_ref()?;
|
||||
let buffer_state = editor_state.active_buffer.as_ref()?;
|
||||
let active_layer = buffer_state.active_layer.clone()?;
|
||||
let active_buffer = buffer_state.buffer.read(cx).snapshot();
|
||||
|
||||
let view = cx.weak_entity();
|
||||
Some(
|
||||
PopoverMenu::new("Syntax Tree")
|
||||
.trigger(Self::render_header(&active_layer))
|
||||
.menu(move |window, cx| {
|
||||
ContextMenu::build(window, cx, |mut menu, _, _| {
|
||||
for (layer_ix, layer) in active_buffer.syntax_layers().enumerate() {
|
||||
let view = view.clone();
|
||||
menu = menu.entry(
|
||||
format!(
|
||||
"{} {}",
|
||||
layer.language.name(),
|
||||
format_node_range(layer.node())
|
||||
),
|
||||
None,
|
||||
move |window, cx| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.select_layer(layer_ix, window, cx);
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
);
|
||||
}
|
||||
menu
|
||||
})
|
||||
.into()
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn select_layer(
|
||||
&mut self,
|
||||
layer_ix: usize,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<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;
|
||||
cx.notify();
|
||||
view.focus_handle.focus(window, cx);
|
||||
Some(())
|
||||
})
|
||||
}
|
||||
|
||||
fn render_header(active_layer: &OwnedSyntaxLayer) -> ButtonLike {
|
||||
ButtonLike::new("syntax tree header")
|
||||
.child(Label::new(active_layer.language.name()))
|
||||
.child(Label::new(format_node_range(active_layer.node())))
|
||||
}
|
||||
|
||||
fn render_update_button(&mut self, cx: &mut Context<Self>) -> Option<IconButton> {
|
||||
self.tree_view.as_ref().and_then(|view| {
|
||||
view.update(cx, |view, cx| {
|
||||
view.last_active_editor.as_ref().map(|editor| {
|
||||
IconButton::new("syntax-view-update", IconName::RotateCw)
|
||||
.tooltip({
|
||||
let active_tab_name = editor.read_with(cx, |editor, cx| {
|
||||
editor.tab_content_text(Default::default(), cx)
|
||||
});
|
||||
|
||||
Tooltip::text(format!("Update view to '{active_tab_name}'"))
|
||||
})
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.update_active_editor(&Default::default(), window, 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 Render for SyntaxTreeToolbarItemView {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(self.render_menu(cx))
|
||||
.children(self.render_update_button(cx))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ToolbarItemEvent> for SyntaxTreeToolbarItemView {}
|
||||
|
||||
impl ToolbarItemView for SyntaxTreeToolbarItemView {
|
||||
fn set_active_pane_item(
|
||||
&mut self,
|
||||
active_pane_item: Option<&dyn ItemHandle>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolbarItemLocation {
|
||||
if let Some(item) = active_pane_item
|
||||
&& let Some(view) = item.downcast::<SyntaxTreeView>()
|
||||
{
|
||||
self.tree_view = Some(view.clone());
|
||||
self.subscription = Some(cx.observe_in(&view, window, |_, _, _, cx| cx.notify()));
|
||||
return ToolbarItemLocation::PrimaryLeft;
|
||||
}
|
||||
self.tree_view = None;
|
||||
self.subscription = None;
|
||||
ToolbarItemLocation::Hidden
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user