
There's still a bit more work to do on this, but this PR is compiling (with warnings) after eliminating the key types. When the tasks below are complete, this will be the new narrative for GPUI: - `Entity<T>` - This replaces `View<T>`/`Model<T>`. It represents a unit of state, and if `T` implements `Render`, then `Entity<T>` implements `Element`. - `&mut App` This replaces `AppContext` and represents the app. - `&mut Context<T>` This replaces `ModelContext` and derefs to `App`. It is provided by the framework when updating an entity. - `&mut Window` Broken out of `&mut WindowContext` which no longer exists. Every method that once took `&mut WindowContext` now takes `&mut Window, &mut App` and every method that took `&mut ViewContext<T>` now takes `&mut Window, &mut Context<T>` Not pictured here are the two other failed attempts. It's been quite a month! Tasks: - [x] Remove `View`, `ViewContext`, `WindowContext` and thread through `Window` - [x] [@cole-miller @mikayla-maki] Redraw window when entities change - [x] [@cole-miller @mikayla-maki] Get examples and Zed running - [x] [@cole-miller @mikayla-maki] Fix Zed rendering - [x] [@mikayla-maki] Fix todo! macros and comments - [x] Fix a bug where the editor would not be redrawn because of view caching - [x] remove publicness window.notify() and replace with `AppContext::notify` - [x] remove `observe_new_window_models`, replace with `observe_new_models` with an optional window - [x] Fix a bug where the project panel would not be redrawn because of the wrong refresh() call being used - [x] Fix the tests - [x] Fix warnings by eliminating `Window` params or using `_` - [x] Fix conflicts - [x] Simplify generic code where possible - [x] Rename types - [ ] Update docs ### issues post merge - [x] Issues switching between normal and insert mode - [x] Assistant re-rendering failure - [x] Vim test failures - [x] Mac build issue Release Notes: - N/A --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Mikayla <mikayla@zed.dev> Co-authored-by: Joseph <joseph@zed.dev> Co-authored-by: max <max@zed.dev> Co-authored-by: Michael Sloan <michael@zed.dev> Co-authored-by: Mikayla Maki <mikaylamaki@Mikaylas-MacBook-Pro.local> Co-authored-by: Mikayla <mikayla.c.maki@gmail.com> Co-authored-by: joão <joao@zed.dev>
295 lines
12 KiB
Rust
295 lines
12 KiB
Rust
use gpui::{
|
|
actions, Action, App, AppContext as _, Entity, EventEmitter, FocusHandle, Focusable,
|
|
KeyBindingContextPredicate, KeyContext, Keystroke, MouseButton, Render, Subscription,
|
|
};
|
|
use itertools::Itertools;
|
|
use serde_json::json;
|
|
use settings::get_key_equivalents;
|
|
use ui::{
|
|
div, h_flex, px, v_flex, ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement,
|
|
Label, LabelCommon, LabelSize, ParentElement, SharedString, StatefulInteractiveElement, Styled,
|
|
Window,
|
|
};
|
|
use ui::{Button, ButtonStyle};
|
|
use workspace::Item;
|
|
use workspace::Workspace;
|
|
|
|
actions!(debug, [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.add_item_to_active_pane(Box::new(key_context_view), None, true, 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, window, 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.context_stack = window.context_stack();
|
|
this.last_keystrokes = Some(
|
|
json!(pending.iter().map(|p| p.unparse()).join(" "))
|
|
.to_string()
|
|
.into(),
|
|
);
|
|
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();
|
|
});
|
|
let sub2 = cx.observe_pending_input(window, |this, window, cx| {
|
|
this.pending_keystrokes = window
|
|
.pending_input_keystrokes()
|
|
.map(|k| k.iter().cloned().collect());
|
|
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 {
|
|
let mut stack = self.context_stack.clone();
|
|
while !stack.is_empty() {
|
|
if predicate.eval(&stack) {
|
|
return true;
|
|
}
|
|
stack.pop();
|
|
}
|
|
false
|
|
}
|
|
|
|
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, _: impl FnMut(workspace::item::ItemEvent)) {}
|
|
|
|
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
|
Some("Keyboard Context".into())
|
|
}
|
|
|
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
|
None
|
|
}
|
|
|
|
fn clone_on_split(
|
|
&self,
|
|
_workspace_id: Option<workspace::WorkspaceId>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Option<Entity<Self>>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
Some(cx.new(|cx| KeyContextView::new(window, cx)))
|
|
}
|
|
}
|
|
|
|
impl Render for KeyContextView {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
|
use itertools::Itertools;
|
|
let key_equivalents = get_key_equivalents(cx.keyboard_layout());
|
|
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("default", "Open Documentation")
|
|
.style(ButtonStyle::Filled)
|
|
.on_click(|_, _, cx| cx.open_url("https://zed.dev/docs/key-bindings")),
|
|
)
|
|
.child(
|
|
Button::new("default", "View default keymap")
|
|
.style(ButtonStyle::Filled)
|
|
.key_binding(ui::KeyBinding::for_action(
|
|
&zed_actions::OpenDefaultKeymap,
|
|
window,
|
|
))
|
|
.on_click(|_, window, cx| {
|
|
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
|
|
window.dispatch_action(zed_actions::OpenDefaultKeymap.boxed_clone(), cx);
|
|
}),
|
|
)
|
|
.child(
|
|
Button::new("default", "Edit your keymap")
|
|
.style(ButtonStyle::Filled)
|
|
.key_binding(ui::KeyBinding::for_action(&zed_actions::OpenKeymap, window))
|
|
.on_click(|_, window, cx| {
|
|
window.dispatch_action(workspace::SplitRight.boxed_clone(), cx);
|
|
window.dispatch_action(zed_actions::OpenKeymap.boxed_clone(), cx);
|
|
}),
|
|
),
|
|
)
|
|
.child(
|
|
Label::new("Current Context Stack")
|
|
.size(LabelSize::Large)
|
|
.mt_8(),
|
|
)
|
|
.children({
|
|
window.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()
|
|
}),
|
|
)
|
|
})
|
|
}
|
|
}
|