Compare commits
6 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
bd4e943597 | ||
![]() |
c5d3c7d790 | ||
![]() |
fff0ecead1 | ||
![]() |
b1b60bb7fe | ||
![]() |
0e575b2809 | ||
![]() |
65c6c709fd |
44 changed files with 5539 additions and 2076 deletions
4
assets/icons/terminal_ghost.svg
Normal file
4
assets/icons/terminal_ghost.svg
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M8 12.375H13" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 11.125L6.75003 7.375L3 3.62497" stroke="black" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 336 B |
1257
assets/images/acp_grid.svg
Normal file
1257
assets/images/acp_grid.svg
Normal file
File diff suppressed because it is too large
Load diff
After Width: | Height: | Size: 176 KiB |
1
assets/images/acp_logo.svg
Normal file
1
assets/images/acp_logo.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="160" height="61" fill="none"><g clip-path="url(#a)"><path fill="#000" d="M130.75.385c5.428 0 10.297 2.81 13.011 7.511l14.214 24.618-.013-.005c2.599 4.504 2.707 9.932.28 14.513-2.618 4.944-7.862 8.015-13.679 8.015h-31.811c-.452 0-.873-.242-1.103-.637a1.268 1.268 0 0 1 0-1.274l3.919-6.78c.223-.394.65-.636 1.102-.636h28.288a5.622 5.622 0 0 0 4.925-2.849 5.615 5.615 0 0 0 0-5.69l-14.214-24.617a5.621 5.621 0 0 0-4.925-2.848 5.621 5.621 0 0 0-4.925 2.848l-14.214 24.618a6.267 6.267 0 0 0-.319.643.998.998 0 0 1-.069.14L101.724 54.4l-.823 1.313-2.529 4.39a1.27 1.27 0 0 1-1.103.636h-7.83c-.452 0-.873-.242-1.102-.637-.23-.394-.23-.879 0-1.274l2.188-3.791H66.803c-3.32 0-6.454-1.122-8.818-3.167a17.141 17.141 0 0 1-3.394-3.96 1.261 1.261 0 0 1-.091-.137L34.2 12.573a5.622 5.622 0 0 0-4.925-2.849 5.621 5.621 0 0 0-4.924 2.85L10.137 37.19a5.615 5.615 0 0 0 0 5.69 5.63 5.63 0 0 0 4.925 2.841h29.862a1.276 1.276 0 0 1 1.102 1.912l-3.912 6.778a1.27 1.27 0 0 1-1.102.638H14.495c-3.32 0-6.454-1.128-8.817-3.173-5.906-5.104-7.36-12.883-3.62-19.363L16.267 7.89C18.872 3.385 23.517.583 28.697.39c.184-.006.356-.006.534-.006 5.378 0 10.45 3.007 13.246 7.85l12.986 22.372L68.58 7.891C71.186 3.385 75.83.582 81.01.39c.185-.006.358-.006.536-.006 4.453 0 8.71 2.039 11.672 5.588.337.407.388.98.127 1.446l-3.765 6.6a1.268 1.268 0 0 1-2.205.006l-.847-1.465a5.623 5.623 0 0 0-4.926-2.848 5.622 5.622 0 0 0-4.924 2.848L62.464 37.18a5.614 5.614 0 0 0 0 5.689 5.628 5.628 0 0 0 4.925 2.842H95.91L117.76 7.87c2.714-4.683 7.575-7.486 12.99-7.486Z"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 .385h160v60.36H0z"/></clipPath></defs></svg>
|
After Width: | Height: | Size: 1.6 KiB |
2
assets/images/acp_logo_serif.svg
Normal file
2
assets/images/acp_logo_serif.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 14 KiB |
1260
assets/keymaps/default-windows.json
Normal file
1260
assets/keymaps/default-windows.json
Normal file
File diff suppressed because it is too large
Load diff
|
@ -6,7 +6,7 @@ use agent2::HistoryStore;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Editor, EditorMode, MinimapVisibility};
|
use editor::{Editor, EditorMode, MinimapVisibility};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable,
|
AnyEntity, App, AppContext as _, Entity, EntityId, EventEmitter, Focusable, ScrollHandle,
|
||||||
TextStyleRefinement, WeakEntity, Window,
|
TextStyleRefinement, WeakEntity, Window,
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
|
@ -154,10 +154,22 @@ impl EntryViewState {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AgentThreadEntry::AssistantMessage(_) => {
|
AgentThreadEntry::AssistantMessage(message) => {
|
||||||
if index == self.entries.len() {
|
let entry = if let Some(Entry::AssistantMessage(entry)) =
|
||||||
self.entries.push(Entry::empty())
|
self.entries.get_mut(index)
|
||||||
}
|
{
|
||||||
|
entry
|
||||||
|
} else {
|
||||||
|
self.set_entry(
|
||||||
|
index,
|
||||||
|
Entry::AssistantMessage(AssistantMessageEntry::default()),
|
||||||
|
);
|
||||||
|
let Some(Entry::AssistantMessage(entry)) = self.entries.get_mut(index) else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
entry
|
||||||
|
};
|
||||||
|
entry.sync(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -177,7 +189,7 @@ impl EntryViewState {
|
||||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||||
for entry in self.entries.iter() {
|
for entry in self.entries.iter() {
|
||||||
match entry {
|
match entry {
|
||||||
Entry::UserMessage { .. } => {}
|
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||||
Entry::Content(response_views) => {
|
Entry::Content(response_views) => {
|
||||||
for view in response_views.values() {
|
for view in response_views.values() {
|
||||||
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
if let Ok(diff_editor) = view.clone().downcast::<Editor>() {
|
||||||
|
@ -208,9 +220,29 @@ pub enum ViewEvent {
|
||||||
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
MessageEditorEvent(Entity<MessageEditor>, MessageEditorEvent),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug)]
|
||||||
|
pub struct AssistantMessageEntry {
|
||||||
|
scroll_handles_by_chunk_index: HashMap<usize, ScrollHandle>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssistantMessageEntry {
|
||||||
|
pub fn scroll_handle_for_chunk(&self, ix: usize) -> Option<ScrollHandle> {
|
||||||
|
self.scroll_handles_by_chunk_index.get(&ix).cloned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sync(&mut self, message: &acp_thread::AssistantMessage) {
|
||||||
|
if let Some(acp_thread::AssistantMessageChunk::Thought { .. }) = message.chunks.last() {
|
||||||
|
let ix = message.chunks.len() - 1;
|
||||||
|
let handle = self.scroll_handles_by_chunk_index.entry(ix).or_default();
|
||||||
|
handle.scroll_to_bottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Entry {
|
pub enum Entry {
|
||||||
UserMessage(Entity<MessageEditor>),
|
UserMessage(Entity<MessageEditor>),
|
||||||
|
AssistantMessage(AssistantMessageEntry),
|
||||||
Content(HashMap<EntityId, AnyEntity>),
|
Content(HashMap<EntityId, AnyEntity>),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -218,7 +250,7 @@ impl Entry {
|
||||||
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
pub fn message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||||
match self {
|
match self {
|
||||||
Self::UserMessage(editor) => Some(editor),
|
Self::UserMessage(editor) => Some(editor),
|
||||||
Entry::Content(_) => None,
|
Self::AssistantMessage(_) | Self::Content(_) => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -239,6 +271,16 @@ impl Entry {
|
||||||
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
.map(|entity| entity.downcast::<TerminalView>().unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scroll_handle_for_assistant_message_chunk(
|
||||||
|
&self,
|
||||||
|
chunk_ix: usize,
|
||||||
|
) -> Option<ScrollHandle> {
|
||||||
|
match self {
|
||||||
|
Self::AssistantMessage(message) => message.scroll_handle_for_chunk(chunk_ix),
|
||||||
|
Self::UserMessage(_) | Self::Content(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
fn content_map(&self) -> Option<&HashMap<EntityId, AnyEntity>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Content(map) => Some(map),
|
Self::Content(map) => Some(map),
|
||||||
|
@ -254,7 +296,7 @@ impl Entry {
|
||||||
pub fn has_content(&self) -> bool {
|
pub fn has_content(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Content(map) => !map.is_empty(),
|
Self::Content(map) => !map.is_empty(),
|
||||||
Self::UserMessage(_) => false,
|
Self::UserMessage(_) | Self::AssistantMessage(_) => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,11 @@ use file_icons::FileIcons;
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
Action, Animation, AnimationExt, AnyView, App, BorderStyle, ClickEvent, ClipboardItem,
|
||||||
EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length, ListOffset,
|
CursorStyle, EdgesRefinement, ElementId, Empty, Entity, FocusHandle, Focusable, Hsla, Length,
|
||||||
ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription,
|
ListOffset, ListState, MouseButton, PlatformDisplay, SharedString, Stateful, StyleRefinement,
|
||||||
Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window,
|
Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||||
WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage, point,
|
Window, WindowHandle, div, ease_in_out, linear_color_stop, linear_gradient, list, percentage,
|
||||||
prelude::*, pulsating_between,
|
point, prelude::*, pulsating_between,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ use text::Anchor;
|
||||||
use theme::ThemeSettings;
|
use theme::ThemeSettings;
|
||||||
use ui::{
|
use ui::{
|
||||||
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
Callout, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding, PopoverMenuHandle,
|
||||||
Scrollbar, ScrollbarState, SpinnerLabel, Tooltip, prelude::*,
|
Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
|
||||||
};
|
};
|
||||||
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
|
||||||
use workspace::{CollaboratorId, Workspace};
|
use workspace::{CollaboratorId, Workspace};
|
||||||
|
@ -66,7 +66,6 @@ use crate::{
|
||||||
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
|
||||||
};
|
};
|
||||||
|
|
||||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
|
||||||
pub const MIN_EDITOR_LINES: usize = 4;
|
pub const MIN_EDITOR_LINES: usize = 4;
|
||||||
pub const MAX_EDITOR_LINES: usize = 8;
|
pub const MAX_EDITOR_LINES: usize = 8;
|
||||||
|
|
||||||
|
@ -279,6 +278,7 @@ pub struct AcpThreadView {
|
||||||
editing_message: Option<usize>,
|
editing_message: Option<usize>,
|
||||||
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
prompt_capabilities: Rc<Cell<PromptCapabilities>>,
|
||||||
is_loading_contents: bool,
|
is_loading_contents: bool,
|
||||||
|
install_command_markdown: Entity<Markdown>,
|
||||||
_cancel_task: Option<Task<()>>,
|
_cancel_task: Option<Task<()>>,
|
||||||
_subscriptions: [Subscription; 3],
|
_subscriptions: [Subscription; 3],
|
||||||
}
|
}
|
||||||
|
@ -392,6 +392,7 @@ impl AcpThreadView {
|
||||||
hovered_recent_history_item: None,
|
hovered_recent_history_item: None,
|
||||||
prompt_capabilities,
|
prompt_capabilities,
|
||||||
is_loading_contents: false,
|
is_loading_contents: false,
|
||||||
|
install_command_markdown: cx.new(|cx| Markdown::new("".into(), None, None, cx)),
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
_cancel_task: None,
|
_cancel_task: None,
|
||||||
focus_handle: cx.focus_handle(),
|
focus_handle: cx.focus_handle(),
|
||||||
|
@ -667,7 +668,12 @@ impl AcpThreadView {
|
||||||
match &self.thread_state {
|
match &self.thread_state {
|
||||||
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
|
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
|
||||||
ThreadState::Loading { .. } => "Loading…".into(),
|
ThreadState::Loading { .. } => "Loading…".into(),
|
||||||
ThreadState::LoadError(_) => "Failed to load".into(),
|
ThreadState::LoadError(error) => match error {
|
||||||
|
LoadError::NotInstalled { .. } => format!("Install {}", self.agent.name()).into(),
|
||||||
|
LoadError::Unsupported { .. } => format!("Upgrade {}", self.agent.name()).into(),
|
||||||
|
LoadError::Exited { .. } => format!("{} Exited", self.agent.name()).into(),
|
||||||
|
LoadError::Other(_) => format!("Error Loading {}", self.agent.name()).into(),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1334,6 +1340,10 @@ impl AcpThreadView {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let is_generating = self
|
||||||
|
.thread()
|
||||||
|
.is_some_and(|thread| thread.read(cx).status() != ThreadStatus::Idle);
|
||||||
|
|
||||||
let primary = match &entry {
|
let primary = match &entry {
|
||||||
AgentThreadEntry::UserMessage(message) => {
|
AgentThreadEntry::UserMessage(message) => {
|
||||||
let Some(editor) = self
|
let Some(editor) = self
|
||||||
|
@ -1493,6 +1503,20 @@ impl AcpThreadView {
|
||||||
.into_any()
|
.into_any()
|
||||||
}
|
}
|
||||||
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
|
||||||
|
let is_last = entry_ix + 1 == total_entries;
|
||||||
|
let pending_thinking_chunk_ix = if is_generating && is_last {
|
||||||
|
chunks
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.next_back()
|
||||||
|
.filter(|(_, segment)| {
|
||||||
|
matches!(segment, AssistantMessageChunk::Thought { .. })
|
||||||
|
})
|
||||||
|
.map(|(index, _)| index)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let style = default_markdown_style(false, false, window, cx);
|
let style = default_markdown_style(false, false, window, cx);
|
||||||
let message_body = v_flex()
|
let message_body = v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -1511,6 +1535,7 @@ impl AcpThreadView {
|
||||||
entry_ix,
|
entry_ix,
|
||||||
chunk_ix,
|
chunk_ix,
|
||||||
md.clone(),
|
md.clone(),
|
||||||
|
Some(chunk_ix) == pending_thinking_chunk_ix,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
@ -1524,7 +1549,7 @@ impl AcpThreadView {
|
||||||
v_flex()
|
v_flex()
|
||||||
.px_5()
|
.px_5()
|
||||||
.py_1()
|
.py_1()
|
||||||
.when(entry_ix + 1 == total_entries, |this| this.pb_4())
|
.when(is_last, |this| this.pb_4())
|
||||||
.w_full()
|
.w_full()
|
||||||
.text_ui(cx)
|
.text_ui(cx)
|
||||||
.child(message_body)
|
.child(message_body)
|
||||||
|
@ -1533,7 +1558,7 @@ impl AcpThreadView {
|
||||||
AgentThreadEntry::ToolCall(tool_call) => {
|
AgentThreadEntry::ToolCall(tool_call) => {
|
||||||
let has_terminals = tool_call.terminals().next().is_some();
|
let has_terminals = tool_call.terminals().next().is_some();
|
||||||
|
|
||||||
div().w_full().py_1().px_5().map(|this| {
|
div().w_full().map(|this| {
|
||||||
if has_terminals {
|
if has_terminals {
|
||||||
this.children(tool_call.terminals().map(|terminal| {
|
this.children(tool_call.terminals().map(|terminal| {
|
||||||
self.render_terminal_tool_call(
|
self.render_terminal_tool_call(
|
||||||
|
@ -1609,64 +1634,90 @@ impl AcpThreadView {
|
||||||
entry_ix: usize,
|
entry_ix: usize,
|
||||||
chunk_ix: usize,
|
chunk_ix: usize,
|
||||||
chunk: Entity<Markdown>,
|
chunk: Entity<Markdown>,
|
||||||
|
pending: bool,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
|
let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
|
||||||
let card_header_id = SharedString::from("inner-card-header");
|
let card_header_id = SharedString::from("inner-card-header");
|
||||||
|
|
||||||
let key = (entry_ix, chunk_ix);
|
let key = (entry_ix, chunk_ix);
|
||||||
|
|
||||||
let is_open = self.expanded_thinking_blocks.contains(&key);
|
let is_open = self.expanded_thinking_blocks.contains(&key);
|
||||||
|
let editor_bg = cx.theme().colors().editor_background;
|
||||||
|
let gradient_overlay = div()
|
||||||
|
.rounded_b_lg()
|
||||||
|
.h_full()
|
||||||
|
.absolute()
|
||||||
|
.w_full()
|
||||||
|
.bottom_0()
|
||||||
|
.left_0()
|
||||||
|
.bg(linear_gradient(
|
||||||
|
180.,
|
||||||
|
linear_color_stop(editor_bg, 1.),
|
||||||
|
linear_color_stop(editor_bg.opacity(0.2), 0.),
|
||||||
|
));
|
||||||
|
|
||||||
|
let scroll_handle = self
|
||||||
|
.entry_view_state
|
||||||
|
.read(cx)
|
||||||
|
.entry(entry_ix)
|
||||||
|
.and_then(|entry| entry.scroll_handle_for_assistant_message_chunk(chunk_ix));
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.rounded_md()
|
||||||
|
.border_1()
|
||||||
|
.border_color(self.tool_card_border_color(cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(header_id)
|
.id(header_id)
|
||||||
.group(&card_header_id)
|
.group(&card_header_id)
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_1p5()
|
.py_0p5()
|
||||||
|
.px_1p5()
|
||||||
|
.rounded_t_md()
|
||||||
|
.bg(self.tool_card_header_bg(cx))
|
||||||
|
.justify_between()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(self.tool_card_border_color(cx))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.size_4()
|
.h(window.line_height())
|
||||||
.justify_center()
|
.gap_1p5()
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.group_hover(&card_header_id, |s| s.invisible().w_0())
|
|
||||||
.child(
|
.child(
|
||||||
Icon::new(IconName::ToolThink)
|
Icon::new(IconName::ToolThink)
|
||||||
.size(IconSize::Small)
|
.size(IconSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.absolute()
|
|
||||||
.inset_0()
|
|
||||||
.invisible()
|
|
||||||
.justify_center()
|
|
||||||
.group_hover(&card_header_id, |s| s.visible())
|
|
||||||
.child(
|
|
||||||
Disclosure::new(("expand", entry_ix), is_open)
|
|
||||||
.opened_icon(IconName::ChevronUp)
|
|
||||||
.closed_icon(IconName::ChevronRight)
|
|
||||||
.on_click(cx.listener({
|
|
||||||
move |this, _event, _window, cx| {
|
|
||||||
if is_open {
|
|
||||||
this.expanded_thinking_blocks.remove(&key);
|
|
||||||
} else {
|
|
||||||
this.expanded_thinking_blocks.insert(key);
|
|
||||||
}
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.text_size(self.tool_name_font_size())
|
.text_size(self.tool_name_font_size())
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.child("Thinking"),
|
.map(|this| {
|
||||||
|
if pending {
|
||||||
|
this.child("Thinking")
|
||||||
|
} else {
|
||||||
|
this.child("Thought Process")
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Disclosure::new(("expand", entry_ix), is_open)
|
||||||
|
.opened_icon(IconName::ChevronUp)
|
||||||
|
.closed_icon(IconName::ChevronDown)
|
||||||
|
.visible_on_hover(&card_header_id)
|
||||||
|
.on_click(cx.listener({
|
||||||
|
move |this, _event, _window, cx| {
|
||||||
|
if is_open {
|
||||||
|
this.expanded_thinking_blocks.remove(&key);
|
||||||
|
} else {
|
||||||
|
this.expanded_thinking_blocks.insert(key);
|
||||||
|
}
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
})),
|
||||||
)
|
)
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
move |this, _event, _window, cx| {
|
move |this, _event, _window, cx| {
|
||||||
|
@ -1679,22 +1730,28 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when(is_open, |this| {
|
.child(
|
||||||
this.child(
|
|
||||||
div()
|
div()
|
||||||
.relative()
|
.relative()
|
||||||
.mt_1p5()
|
.bg(editor_bg)
|
||||||
.ml(rems(0.4))
|
.rounded_b_lg()
|
||||||
.pl_4()
|
.child(
|
||||||
.border_l_1()
|
div()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.id(("thinking-content", chunk_ix))
|
||||||
|
.when_some(scroll_handle, |this, scroll_handle| {
|
||||||
|
this.track_scroll(&scroll_handle)
|
||||||
|
})
|
||||||
|
.p_2()
|
||||||
|
.when(!is_open, |this| this.max_h_20())
|
||||||
.text_ui_sm(cx)
|
.text_ui_sm(cx)
|
||||||
|
.overflow_hidden()
|
||||||
.child(self.render_markdown(
|
.child(self.render_markdown(
|
||||||
chunk,
|
chunk,
|
||||||
default_markdown_style(false, false, window, cx),
|
default_markdown_style(false, false, window, cx),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
})
|
.when(!is_open && pending, |this| this.child(gradient_overlay)),
|
||||||
|
)
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1705,7 +1762,6 @@ impl AcpThreadView {
|
||||||
window: &Window,
|
window: &Window,
|
||||||
cx: &Context<Self>,
|
cx: &Context<Self>,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
let header_id = SharedString::from(format!("outer-tool-call-header-{}", entry_ix));
|
|
||||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||||
|
|
||||||
let tool_icon =
|
let tool_icon =
|
||||||
|
@ -1734,11 +1790,7 @@ impl AcpThreadView {
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let failed_tool_call = matches!(
|
let has_location = tool_call.locations.len() == 1;
|
||||||
tool_call.status,
|
|
||||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed
|
|
||||||
);
|
|
||||||
|
|
||||||
let needs_confirmation = matches!(
|
let needs_confirmation = matches!(
|
||||||
tool_call.status,
|
tool_call.status,
|
||||||
ToolCallStatus::WaitingForConfirmation { .. }
|
ToolCallStatus::WaitingForConfirmation { .. }
|
||||||
|
@ -1751,23 +1803,31 @@ impl AcpThreadView {
|
||||||
|
|
||||||
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
let is_open = needs_confirmation || self.expanded_tool_calls.contains(&tool_call.id);
|
||||||
|
|
||||||
let gradient_overlay = |color: Hsla| {
|
let gradient_overlay = {
|
||||||
div()
|
div()
|
||||||
.absolute()
|
.absolute()
|
||||||
.top_0()
|
.top_0()
|
||||||
.right_0()
|
.right_0()
|
||||||
.w_12()
|
.w_12()
|
||||||
.h_full()
|
.h_full()
|
||||||
.bg(linear_gradient(
|
.map(|this| {
|
||||||
|
if use_card_layout {
|
||||||
|
this.bg(linear_gradient(
|
||||||
90.,
|
90.,
|
||||||
linear_color_stop(color, 1.),
|
linear_color_stop(self.tool_card_header_bg(cx), 1.),
|
||||||
linear_color_stop(color.opacity(0.2), 0.),
|
linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
|
||||||
))
|
))
|
||||||
};
|
|
||||||
let gradient_color = if use_card_layout {
|
|
||||||
self.tool_card_header_bg(cx)
|
|
||||||
} else {
|
} else {
|
||||||
cx.theme().colors().panel_background
|
this.bg(linear_gradient(
|
||||||
|
90.,
|
||||||
|
linear_color_stop(cx.theme().colors().panel_background, 1.),
|
||||||
|
linear_color_stop(
|
||||||
|
cx.theme().colors().panel_background.opacity(0.2),
|
||||||
|
0.,
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
let tool_output_display = if is_open {
|
let tool_output_display = if is_open {
|
||||||
|
@ -1818,41 +1878,58 @@ impl AcpThreadView {
|
||||||
};
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.when(use_card_layout, |this| {
|
.map(|this| {
|
||||||
this.rounded_md()
|
if use_card_layout {
|
||||||
|
this.my_2()
|
||||||
|
.rounded_md()
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.border_color(self.tool_card_border_color(cx))
|
||||||
.bg(cx.theme().colors().editor_background)
|
.bg(cx.theme().colors().editor_background)
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
|
} else {
|
||||||
|
this.my_1()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
.map(|this| {
|
||||||
|
if has_location && !use_card_layout {
|
||||||
|
this.ml_4()
|
||||||
|
} else {
|
||||||
|
this.ml_5()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.mr_5()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(header_id)
|
|
||||||
.group(&card_header_id)
|
.group(&card_header_id)
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
.justify_between()
|
||||||
.when(use_card_layout, |this| {
|
.when(use_card_layout, |this| {
|
||||||
this.pl_1p5()
|
this.p_0p5()
|
||||||
.pr_1()
|
|
||||||
.py_0p5()
|
|
||||||
.rounded_t_md()
|
.rounded_t_md()
|
||||||
.when(is_open && !failed_tool_call, |this| {
|
.bg(self.tool_card_header_bg(cx))
|
||||||
|
.when(is_open && !failed_or_canceled, |this| {
|
||||||
this.border_b_1()
|
this.border_b_1()
|
||||||
.border_color(self.tool_card_border_color(cx))
|
.border_color(self.tool_card_border_color(cx))
|
||||||
})
|
})
|
||||||
.bg(self.tool_card_header_bg(cx))
|
|
||||||
})
|
})
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.relative()
|
.relative()
|
||||||
.w_full()
|
.w_full()
|
||||||
.h(window.line_height() - px(2.))
|
.h(window.line_height())
|
||||||
.text_size(self.tool_name_font_size())
|
.text_size(self.tool_name_font_size())
|
||||||
.gap_0p5()
|
.gap_1p5()
|
||||||
|
.when(has_location || use_card_layout, |this| this.px_1())
|
||||||
|
.when(has_location, |this| {
|
||||||
|
this.cursor(CursorStyle::PointingHand)
|
||||||
|
.rounded_sm()
|
||||||
|
.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.5)))
|
||||||
|
})
|
||||||
|
.overflow_hidden()
|
||||||
.child(tool_icon)
|
.child(tool_icon)
|
||||||
.child(if tool_call.locations.len() == 1 {
|
.child(if has_location {
|
||||||
let name = tool_call.locations[0]
|
let name = tool_call.locations[0]
|
||||||
.path
|
.path
|
||||||
.file_name()
|
.file_name()
|
||||||
|
@ -1863,13 +1940,6 @@ impl AcpThreadView {
|
||||||
h_flex()
|
h_flex()
|
||||||
.id(("open-tool-call-location", entry_ix))
|
.id(("open-tool-call-location", entry_ix))
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
|
||||||
.px_1p5()
|
|
||||||
.rounded_sm()
|
|
||||||
.overflow_x_scroll()
|
|
||||||
.hover(|label| {
|
|
||||||
label.bg(cx.theme().colors().element_hover.opacity(0.5))
|
|
||||||
})
|
|
||||||
.map(|this| {
|
.map(|this| {
|
||||||
if use_card_layout {
|
if use_card_layout {
|
||||||
this.text_color(cx.theme().colors().text)
|
this.text_color(cx.theme().colors().text)
|
||||||
|
@ -1879,28 +1949,25 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.child(name)
|
.child(name)
|
||||||
.tooltip(Tooltip::text("Jump to File"))
|
.tooltip(Tooltip::text("Jump to File"))
|
||||||
.cursor(gpui::CursorStyle::PointingHand)
|
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||||
}))
|
}))
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
} else {
|
} else {
|
||||||
h_flex()
|
h_flex()
|
||||||
.relative()
|
|
||||||
.w_full()
|
.w_full()
|
||||||
.max_w_full()
|
.child(self.render_markdown(
|
||||||
.ml_1p5()
|
|
||||||
.overflow_hidden()
|
|
||||||
.child(h_flex().pr_8().child(self.render_markdown(
|
|
||||||
tool_call.label.clone(),
|
tool_call.label.clone(),
|
||||||
default_markdown_style(false, true, window, cx),
|
default_markdown_style(false, true, window, cx),
|
||||||
)))
|
))
|
||||||
.child(gradient_overlay(gradient_color))
|
|
||||||
.into_any()
|
.into_any()
|
||||||
}),
|
})
|
||||||
|
.when(!has_location, |this| this.child(gradient_overlay)),
|
||||||
)
|
)
|
||||||
.child(
|
.when(is_collapsible || failed_or_canceled, |this| {
|
||||||
|
this.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.px_1()
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.when(is_collapsible, |this| {
|
.when(is_collapsible, |this| {
|
||||||
this.child(
|
this.child(
|
||||||
|
@ -1928,7 +1995,8 @@ impl AcpThreadView {
|
||||||
.size(IconSize::Small),
|
.size(IconSize::Small),
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
),
|
)
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
.children(tool_output_display)
|
.children(tool_output_display)
|
||||||
}
|
}
|
||||||
|
@ -2214,6 +2282,12 @@ impl AcpThreadView {
|
||||||
started_at.elapsed()
|
started_at.elapsed()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let header_id =
|
||||||
|
SharedString::from(format!("terminal-tool-header-{}", terminal.entity_id()));
|
||||||
|
let header_group = SharedString::from(format!(
|
||||||
|
"terminal-tool-header-group-{}",
|
||||||
|
terminal.entity_id()
|
||||||
|
));
|
||||||
let header_bg = cx
|
let header_bg = cx
|
||||||
.theme()
|
.theme()
|
||||||
.colors()
|
.colors()
|
||||||
|
@ -2229,10 +2303,7 @@ impl AcpThreadView {
|
||||||
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
let is_expanded = self.expanded_tool_calls.contains(&tool_call.id);
|
||||||
|
|
||||||
let header = h_flex()
|
let header = h_flex()
|
||||||
.id(SharedString::from(format!(
|
.id(header_id)
|
||||||
"terminal-tool-header-{}",
|
|
||||||
terminal.entity_id()
|
|
||||||
)))
|
|
||||||
.flex_none()
|
.flex_none()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
|
@ -2296,23 +2367,6 @@ impl AcpThreadView {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.when(tool_failed || command_failed, |header| {
|
|
||||||
header.child(
|
|
||||||
div()
|
|
||||||
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
|
|
||||||
.child(
|
|
||||||
Icon::new(IconName::Close)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Error),
|
|
||||||
)
|
|
||||||
.when_some(output.and_then(|o| o.exit_status), |this, status| {
|
|
||||||
this.tooltip(Tooltip::text(format!(
|
|
||||||
"Exited with code {}",
|
|
||||||
status.code().unwrap_or(-1),
|
|
||||||
)))
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.when(truncated_output, |header| {
|
.when(truncated_output, |header| {
|
||||||
let tooltip = if let Some(output) = output {
|
let tooltip = if let Some(output) = output {
|
||||||
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
if output_line_count + 10 > terminal::MAX_SCROLL_HISTORY_LINES {
|
||||||
|
@ -2365,6 +2419,7 @@ impl AcpThreadView {
|
||||||
)
|
)
|
||||||
.opened_icon(IconName::ChevronUp)
|
.opened_icon(IconName::ChevronUp)
|
||||||
.closed_icon(IconName::ChevronDown)
|
.closed_icon(IconName::ChevronDown)
|
||||||
|
.visible_on_hover(&header_group)
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
let id = tool_call.id.clone();
|
let id = tool_call.id.clone();
|
||||||
move |this, _event, _window, _cx| {
|
move |this, _event, _window, _cx| {
|
||||||
|
@ -2373,8 +2428,26 @@ impl AcpThreadView {
|
||||||
} else {
|
} else {
|
||||||
this.expanded_tool_calls.insert(id.clone());
|
this.expanded_tool_calls.insert(id.clone());
|
||||||
}
|
}
|
||||||
}})),
|
}
|
||||||
);
|
})),
|
||||||
|
)
|
||||||
|
.when(tool_failed || command_failed, |header| {
|
||||||
|
header.child(
|
||||||
|
div()
|
||||||
|
.id(("terminal-tool-error-code-indicator", terminal.entity_id()))
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Close)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Error),
|
||||||
|
)
|
||||||
|
.when_some(output.and_then(|o| o.exit_status), |this, status| {
|
||||||
|
this.tooltip(Tooltip::text(format!(
|
||||||
|
"Exited with code {}",
|
||||||
|
status.code().unwrap_or(-1),
|
||||||
|
)))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
let terminal_view = self
|
let terminal_view = self
|
||||||
.entry_view_state
|
.entry_view_state
|
||||||
|
@ -2384,7 +2457,8 @@ impl AcpThreadView {
|
||||||
let show_output = is_expanded && terminal_view.is_some();
|
let show_output = is_expanded && terminal_view.is_some();
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.mb_2()
|
.my_2()
|
||||||
|
.mx_5()
|
||||||
.border_1()
|
.border_1()
|
||||||
.when(tool_failed || command_failed, |card| card.border_dashed())
|
.when(tool_failed || command_failed, |card| card.border_dashed())
|
||||||
.border_color(border_color)
|
.border_color(border_color)
|
||||||
|
@ -2392,9 +2466,10 @@ impl AcpThreadView {
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(
|
.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
|
.group(&header_group)
|
||||||
.py_1p5()
|
.py_1p5()
|
||||||
.pl_2()
|
|
||||||
.pr_1p5()
|
.pr_1p5()
|
||||||
|
.pl_2()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
.bg(header_bg)
|
.bg(header_bg)
|
||||||
.text_xs()
|
.text_xs()
|
||||||
|
@ -2766,26 +2841,46 @@ impl AcpThreadView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_load_error(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
|
fn render_load_error(
|
||||||
let (message, action_slot) = match e {
|
&self,
|
||||||
|
e: &LoadError,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> AnyElement {
|
||||||
|
let (message, action_slot): (SharedString, _) = match e {
|
||||||
LoadError::NotInstalled {
|
LoadError::NotInstalled {
|
||||||
error_message,
|
error_message: _,
|
||||||
install_message,
|
install_message: _,
|
||||||
install_command,
|
install_command,
|
||||||
} => {
|
} => {
|
||||||
let install_command = install_command.clone();
|
return self.render_not_installed(install_command.clone(), false, window, cx);
|
||||||
let button = Button::new("install", install_message)
|
}
|
||||||
.tooltip(Tooltip::text(install_command.clone()))
|
LoadError::Unsupported {
|
||||||
.style(ButtonStyle::Outlined)
|
error_message: _,
|
||||||
.label_size(LabelSize::Small)
|
upgrade_message: _,
|
||||||
.icon(IconName::Download)
|
upgrade_command,
|
||||||
.icon_size(IconSize::Small)
|
} => {
|
||||||
.icon_color(Color::Muted)
|
return self.render_not_installed(upgrade_command.clone(), true, window, cx);
|
||||||
.icon_position(IconPosition::Start)
|
}
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
|
||||||
telemetry::event!("Agent Install CLI", agent = this.agent.telemetry_id());
|
LoadError::Other(msg) => (
|
||||||
|
msg.into(),
|
||||||
|
Some(self.create_copy_button(msg.to_string()).into_any_element()),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
let task = this
|
Callout::new()
|
||||||
|
.severity(Severity::Error)
|
||||||
|
.icon(IconName::XCircleFilled)
|
||||||
|
.title("Failed to Launch")
|
||||||
|
.description(message)
|
||||||
|
.actions_slot(div().children(action_slot))
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install_agent(&self, install_command: String, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
telemetry::event!("Agent Install CLI", agent = self.agent.telemetry_id());
|
||||||
|
let task = self
|
||||||
.workspace
|
.workspace
|
||||||
.update(cx, |workspace, cx| {
|
.update(cx, |workspace, cx| {
|
||||||
let project = workspace.project().read(cx);
|
let project = workspace.project().read(cx);
|
||||||
|
@ -2823,82 +2918,78 @@ impl AcpThreadView {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.detach()
|
.detach()
|
||||||
}));
|
|
||||||
|
|
||||||
(error_message.clone(), Some(button.into_any_element()))
|
|
||||||
}
|
}
|
||||||
LoadError::Unsupported {
|
|
||||||
error_message,
|
fn render_not_installed(
|
||||||
upgrade_message,
|
&self,
|
||||||
upgrade_command,
|
install_command: String,
|
||||||
} => {
|
is_upgrade: bool,
|
||||||
let upgrade_command = upgrade_command.clone();
|
window: &mut Window,
|
||||||
let button = Button::new("upgrade", upgrade_message)
|
cx: &mut Context<Self>,
|
||||||
.tooltip(Tooltip::text(upgrade_command.clone()))
|
) -> AnyElement {
|
||||||
.style(ButtonStyle::Outlined)
|
self.install_command_markdown.update(cx, |markdown, cx| {
|
||||||
|
if !markdown.source().contains(&install_command) {
|
||||||
|
markdown.replace(format!("```\n{}\n```", install_command), cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let (heading_label, description_label, button_label, or_label) = if is_upgrade {
|
||||||
|
(
|
||||||
|
"Upgrade Gemini CLI in Zed",
|
||||||
|
"Get access to the latest version with support for Zed.",
|
||||||
|
"Upgrade Gemini CLI",
|
||||||
|
"Or, to upgrade it manually:",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"Get Started with Gemini CLI in Zed",
|
||||||
|
"Use Google's new coding agent directly in Zed.",
|
||||||
|
"Install Gemini CLI",
|
||||||
|
"Or, to install it manually:",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.p_3p5()
|
||||||
|
.gap_2p5()
|
||||||
|
.border_t_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.bg(linear_gradient(
|
||||||
|
180.,
|
||||||
|
linear_color_stop(cx.theme().colors().editor_background.opacity(0.4), 4.),
|
||||||
|
linear_color_stop(cx.theme().status().info_background.opacity(0.), 0.),
|
||||||
|
))
|
||||||
|
.child(
|
||||||
|
v_flex().gap_0p5().child(Label::new(heading_label)).child(
|
||||||
|
Label::new(description_label)
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("install_gemini", button_label)
|
||||||
|
.full_width()
|
||||||
|
.size(ButtonSize::Medium)
|
||||||
|
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.icon(IconName::Download)
|
.icon(IconName::TerminalGhost)
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.icon_color(Color::Muted)
|
.icon_color(Color::Muted)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
telemetry::event!("Agent Upgrade CLI", agent = this.agent.telemetry_id());
|
this.install_agent(install_command.clone(), window, cx)
|
||||||
|
})),
|
||||||
let task = this
|
)
|
||||||
.workspace
|
.child(
|
||||||
.update(cx, |workspace, cx| {
|
Label::new(or_label)
|
||||||
let project = workspace.project().read(cx);
|
.size(LabelSize::Small)
|
||||||
let cwd = project.first_project_directory(cx);
|
.color(Color::Muted),
|
||||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
)
|
||||||
let spawn_in_terminal = task::SpawnInTerminal {
|
.child(MarkdownElement::new(
|
||||||
id: task::TaskId(upgrade_command.to_string()),
|
self.install_command_markdown.clone(),
|
||||||
full_label: upgrade_command.clone(),
|
default_markdown_style(false, false, window, cx),
|
||||||
label: upgrade_command.clone(),
|
))
|
||||||
command: Some(upgrade_command.clone()),
|
|
||||||
args: Vec::new(),
|
|
||||||
command_label: upgrade_command.clone(),
|
|
||||||
cwd,
|
|
||||||
env: Default::default(),
|
|
||||||
use_new_terminal: true,
|
|
||||||
allow_concurrent_runs: true,
|
|
||||||
reveal: Default::default(),
|
|
||||||
reveal_target: Default::default(),
|
|
||||||
hide: Default::default(),
|
|
||||||
shell,
|
|
||||||
show_summary: true,
|
|
||||||
show_command: true,
|
|
||||||
show_rerun: false,
|
|
||||||
};
|
|
||||||
workspace.spawn_in_terminal(spawn_in_terminal, window, cx)
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
let Some(task) = task else { return };
|
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
|
||||||
if let Some(Ok(_)) = task.await {
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
|
||||||
this.reset(window, cx);
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach()
|
|
||||||
}));
|
|
||||||
|
|
||||||
(error_message.clone(), Some(button.into_any_element()))
|
|
||||||
}
|
|
||||||
LoadError::Exited { .. } => ("Server exited with status {status}".into(), None),
|
|
||||||
LoadError::Other(msg) => (
|
|
||||||
msg.into(),
|
|
||||||
Some(self.create_copy_button(msg.to_string()).into_any_element()),
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
Callout::new()
|
|
||||||
.severity(Severity::Error)
|
|
||||||
.icon(IconName::XCircleFilled)
|
|
||||||
.title("Failed to Launch")
|
|
||||||
.description(message)
|
|
||||||
.actions_slot(div().children(action_slot))
|
|
||||||
.into_any_element()
|
.into_any_element()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4153,13 +4244,14 @@ impl AcpThreadView {
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
|
||||||
if is_generating {
|
if is_generating {
|
||||||
return h_flex().id("thread-controls-container").ml_1().child(
|
return h_flex().id("thread-controls-container").child(
|
||||||
div()
|
div()
|
||||||
.py_2()
|
.py_2()
|
||||||
.px(rems_from_px(22.))
|
.px_5()
|
||||||
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
.child(SpinnerLabel::new().size(LabelSize::Small)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
let open_as_markdown = IconButton::new("open-as-markdown", IconName::FileMarkdown)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
|
@ -4185,12 +4277,10 @@ impl AcpThreadView {
|
||||||
.id("thread-controls-container")
|
.id("thread-controls-container")
|
||||||
.group("thread-controls-container")
|
.group("thread-controls-container")
|
||||||
.w_full()
|
.w_full()
|
||||||
.mr_1()
|
.py_2()
|
||||||
.pt_1()
|
.px_5()
|
||||||
.pb_2()
|
|
||||||
.px(RESPONSE_PADDING_X)
|
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.opacity(0.4)
|
.opacity(0.6)
|
||||||
.hover(|style| style.opacity(1.))
|
.hover(|style| style.opacity(1.))
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.justify_end();
|
.justify_end();
|
||||||
|
@ -4201,21 +4291,24 @@ impl AcpThreadView {
|
||||||
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
|
.is_some_and(|thread| thread.read(cx).connection().telemetry().is_some())
|
||||||
{
|
{
|
||||||
let feedback = self.thread_feedback.feedback;
|
let feedback = self.thread_feedback.feedback;
|
||||||
container = container.child(
|
|
||||||
|
container = container
|
||||||
|
.child(
|
||||||
div().visible_on_hover("thread-controls-container").child(
|
div().visible_on_hover("thread-controls-container").child(
|
||||||
Label::new(
|
Label::new(match feedback {
|
||||||
match feedback {
|
|
||||||
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
|
Some(ThreadFeedback::Positive) => "Thanks for your feedback!",
|
||||||
Some(ThreadFeedback::Negative) => "We appreciate your feedback and will use it to improve.",
|
Some(ThreadFeedback::Negative) => {
|
||||||
None => "Rating the thread sends all of your current conversation to the Zed team.",
|
"We appreciate your feedback and will use it to improve."
|
||||||
}
|
}
|
||||||
)
|
None => {
|
||||||
|
"Rating the thread sends all of your current conversation to the Zed team."
|
||||||
|
}
|
||||||
|
})
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(LabelSize::XSmall)
|
.size(LabelSize::XSmall)
|
||||||
.truncate(),
|
.truncate(),
|
||||||
),
|
),
|
||||||
).child(
|
)
|
||||||
h_flex()
|
|
||||||
.child(
|
.child(
|
||||||
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
IconButton::new("feedback-thumbs-up", IconName::ThumbsUp)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
|
@ -4226,11 +4319,7 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.tooltip(Tooltip::text("Helpful Response"))
|
.tooltip(Tooltip::text("Helpful Response"))
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.handle_feedback_click(
|
this.handle_feedback_click(ThreadFeedback::Positive, window, cx);
|
||||||
ThreadFeedback::Positive,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
|
@ -4243,14 +4332,9 @@ impl AcpThreadView {
|
||||||
})
|
})
|
||||||
.tooltip(Tooltip::text("Not Helpful"))
|
.tooltip(Tooltip::text("Not Helpful"))
|
||||||
.on_click(cx.listener(move |this, _, window, cx| {
|
.on_click(cx.listener(move |this, _, window, cx| {
|
||||||
this.handle_feedback_click(
|
this.handle_feedback_click(ThreadFeedback::Negative, window, cx);
|
||||||
ThreadFeedback::Negative,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})),
|
})),
|
||||||
)
|
);
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
container.child(open_as_markdown).child(scroll_to_top)
|
container.child(open_as_markdown).child(scroll_to_top)
|
||||||
|
@ -4882,7 +4966,7 @@ impl Render for AcpThreadView {
|
||||||
.size_full()
|
.size_full()
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_end()
|
.justify_end()
|
||||||
.child(self.render_load_error(e, cx)),
|
.child(self.render_load_error(e, window, cx)),
|
||||||
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
|
ThreadState::Ready { .. } => v_flex().flex_1().map(|this| {
|
||||||
if has_messages {
|
if has_messages {
|
||||||
this.child(
|
this.child(
|
||||||
|
|
|
@ -1093,7 +1093,7 @@ impl AgentConfiguration {
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
Label::new(
|
Label::new(
|
||||||
"Use the full power of Zed's UI with your favorite agent, connected via the Agent Client Protocol.",
|
"Bring the agent of your choice to Zed via our new Agent Client Protocol.",
|
||||||
)
|
)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
),
|
),
|
||||||
|
|
|
@ -14,6 +14,7 @@ use zed_actions::agent::ReauthenticateAgent;
|
||||||
|
|
||||||
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
use crate::acp::{AcpThreadHistory, ThreadHistoryEvent};
|
||||||
use crate::agent_diff::AgentDiffThread;
|
use crate::agent_diff::AgentDiffThread;
|
||||||
|
use crate::ui::AcpOnboardingModal;
|
||||||
use crate::{
|
use crate::{
|
||||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||||
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
|
||||||
|
@ -77,7 +78,10 @@ use workspace::{
|
||||||
};
|
};
|
||||||
use zed_actions::{
|
use zed_actions::{
|
||||||
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
|
||||||
agent::{OpenOnboardingModal, OpenSettings, ResetOnboarding, ToggleModelSelector},
|
agent::{
|
||||||
|
OpenAcpOnboardingModal, OpenOnboardingModal, OpenSettings, ResetOnboarding,
|
||||||
|
ToggleModelSelector,
|
||||||
|
},
|
||||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -201,6 +205,9 @@ pub fn init(cx: &mut App) {
|
||||||
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
.register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
|
||||||
AgentOnboardingModal::toggle(workspace, window, cx)
|
AgentOnboardingModal::toggle(workspace, window, cx)
|
||||||
})
|
})
|
||||||
|
.register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
|
||||||
|
AcpOnboardingModal::toggle(workspace, window, cx)
|
||||||
|
})
|
||||||
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
.register_action(|_workspace, _: &ResetOnboarding, window, cx| {
|
||||||
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
|
||||||
window.refresh();
|
window.refresh();
|
||||||
|
@ -1841,19 +1848,6 @@ impl AgentPanel {
|
||||||
menu
|
menu
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_selected_agent(
|
|
||||||
&mut self,
|
|
||||||
agent: AgentType,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) {
|
|
||||||
if self.selected_agent != agent {
|
|
||||||
self.selected_agent = agent.clone();
|
|
||||||
self.serialize(cx);
|
|
||||||
}
|
|
||||||
self.new_agent_thread(agent, window, cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn selected_agent(&self) -> AgentType {
|
pub fn selected_agent(&self) -> AgentType {
|
||||||
self.selected_agent.clone()
|
self.selected_agent.clone()
|
||||||
}
|
}
|
||||||
|
@ -1864,6 +1858,11 @@ impl AgentPanel {
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if self.selected_agent != agent {
|
||||||
|
self.selected_agent = agent.clone();
|
||||||
|
self.serialize(cx);
|
||||||
|
}
|
||||||
|
|
||||||
match agent {
|
match agent {
|
||||||
AgentType::Zed => {
|
AgentType::Zed => {
|
||||||
window.dispatch_action(
|
window.dispatch_action(
|
||||||
|
@ -2544,7 +2543,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.set_selected_agent(
|
panel.new_agent_thread(
|
||||||
AgentType::NativeAgent,
|
AgentType::NativeAgent,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2570,7 +2569,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.set_selected_agent(
|
panel.new_agent_thread(
|
||||||
AgentType::TextThread,
|
AgentType::TextThread,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2598,7 +2597,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.set_selected_agent(
|
panel.new_agent_thread(
|
||||||
AgentType::Gemini,
|
AgentType::Gemini,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2625,7 +2624,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.set_selected_agent(
|
panel.new_agent_thread(
|
||||||
AgentType::ClaudeCode,
|
AgentType::ClaudeCode,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
@ -2658,7 +2657,7 @@ impl AgentPanel {
|
||||||
workspace.panel::<AgentPanel>(cx)
|
workspace.panel::<AgentPanel>(cx)
|
||||||
{
|
{
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
panel.set_selected_agent(
|
panel.new_agent_thread(
|
||||||
AgentType::Custom {
|
AgentType::Custom {
|
||||||
name: agent_name
|
name: agent_name
|
||||||
.clone(),
|
.clone(),
|
||||||
|
@ -2682,9 +2681,9 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |menu| {
|
||||||
menu.separator().link(
|
menu.separator().link(
|
||||||
"Add Your Own Agent",
|
"Add Other Agents",
|
||||||
OpenBrowser {
|
OpenBrowser {
|
||||||
url: "https://agentclientprotocol.com/".into(),
|
url: zed_urls::external_agents_docs(cx),
|
||||||
}
|
}
|
||||||
.boxed_clone(),
|
.boxed_clone(),
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod acp_onboarding_modal;
|
||||||
mod agent_notification;
|
mod agent_notification;
|
||||||
mod burn_mode_tooltip;
|
mod burn_mode_tooltip;
|
||||||
mod context_pill;
|
mod context_pill;
|
||||||
|
@ -6,6 +7,7 @@ mod onboarding_modal;
|
||||||
pub mod preview;
|
pub mod preview;
|
||||||
mod unavailable_editing_tooltip;
|
mod unavailable_editing_tooltip;
|
||||||
|
|
||||||
|
pub use acp_onboarding_modal::*;
|
||||||
pub use agent_notification::*;
|
pub use agent_notification::*;
|
||||||
pub use burn_mode_tooltip::*;
|
pub use burn_mode_tooltip::*;
|
||||||
pub use context_pill::*;
|
pub use context_pill::*;
|
||||||
|
|
254
crates/agent_ui/src/ui/acp_onboarding_modal.rs
Normal file
254
crates/agent_ui/src/ui/acp_onboarding_modal.rs
Normal file
|
@ -0,0 +1,254 @@
|
||||||
|
use client::zed_urls;
|
||||||
|
use gpui::{
|
||||||
|
ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, MouseDownEvent, Render,
|
||||||
|
linear_color_stop, linear_gradient,
|
||||||
|
};
|
||||||
|
use ui::{TintColor, Vector, VectorName, prelude::*};
|
||||||
|
use workspace::{ModalView, Workspace};
|
||||||
|
|
||||||
|
use crate::agent_panel::{AgentPanel, AgentType};
|
||||||
|
|
||||||
|
macro_rules! acp_onboarding_event {
|
||||||
|
($name:expr) => {
|
||||||
|
telemetry::event!($name, source = "ACP Onboarding");
|
||||||
|
};
|
||||||
|
($name:expr, $($key:ident $(= $value:expr)?),+ $(,)?) => {
|
||||||
|
telemetry::event!($name, source = "ACP Onboarding", $($key $(= $value)?),+);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AcpOnboardingModal {
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcpOnboardingModal {
|
||||||
|
pub fn toggle(workspace: &mut Workspace, window: &mut Window, cx: &mut Context<Workspace>) {
|
||||||
|
let workspace_entity = cx.entity();
|
||||||
|
workspace.toggle_modal(window, cx, |_window, cx| Self {
|
||||||
|
workspace: workspace_entity,
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn open_panel(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.workspace.update(cx, |workspace, cx| {
|
||||||
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||||
|
|
||||||
|
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||||
|
panel.update(cx, |panel, cx| {
|
||||||
|
panel.new_agent_thread(AgentType::Gemini, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
|
||||||
|
acp_onboarding_event!("Open Panel Clicked");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn view_docs(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.open_url(&zed_urls::external_agents_docs(cx));
|
||||||
|
cx.notify();
|
||||||
|
|
||||||
|
acp_onboarding_event!("Documentation Link Clicked");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for AcpOnboardingModal {}
|
||||||
|
|
||||||
|
impl Focusable for AcpOnboardingModal {
|
||||||
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ModalView for AcpOnboardingModal {}
|
||||||
|
|
||||||
|
impl Render for AcpOnboardingModal {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let illustration_element = |label: bool, opacity: f32| {
|
||||||
|
h_flex()
|
||||||
|
.px_1()
|
||||||
|
.py_0p5()
|
||||||
|
.gap_1()
|
||||||
|
.rounded_sm()
|
||||||
|
.bg(cx.theme().colors().element_active.opacity(0.05))
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.border_dashed()
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Stop)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.15))),
|
||||||
|
)
|
||||||
|
.map(|this| {
|
||||||
|
if label {
|
||||||
|
this.child(
|
||||||
|
Label::new("Your Agent Here")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this.child(
|
||||||
|
div().w_16().h_1().rounded_full().bg(cx
|
||||||
|
.theme()
|
||||||
|
.colors()
|
||||||
|
.element_active
|
||||||
|
.opacity(0.6)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.opacity(opacity)
|
||||||
|
};
|
||||||
|
|
||||||
|
let illustration = h_flex()
|
||||||
|
.relative()
|
||||||
|
.h(rems_from_px(126.))
|
||||||
|
.bg(cx.theme().colors().editor_background)
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.justify_center()
|
||||||
|
.gap_8()
|
||||||
|
.rounded_t_md()
|
||||||
|
.overflow_hidden()
|
||||||
|
.child(
|
||||||
|
div().absolute().inset_0().w(px(515.)).h(px(126.)).child(
|
||||||
|
Vector::new(VectorName::AcpGrid, rems_from_px(515.), rems_from_px(126.))
|
||||||
|
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.02))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(div().absolute().inset_0().size_full().bg(linear_gradient(
|
||||||
|
0.,
|
||||||
|
linear_color_stop(
|
||||||
|
cx.theme().colors().elevated_surface_background.opacity(0.1),
|
||||||
|
0.9,
|
||||||
|
),
|
||||||
|
linear_color_stop(
|
||||||
|
cx.theme().colors().elevated_surface_background.opacity(0.),
|
||||||
|
0.,
|
||||||
|
),
|
||||||
|
)))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.inset_0()
|
||||||
|
.size_full()
|
||||||
|
.bg(gpui::black().opacity(0.15)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_4()
|
||||||
|
.child(
|
||||||
|
Vector::new(VectorName::AcpLogo, rems_from_px(106.), rems_from_px(40.))
|
||||||
|
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Vector::new(
|
||||||
|
VectorName::AcpLogoSerif,
|
||||||
|
rems_from_px(111.),
|
||||||
|
rems_from_px(41.),
|
||||||
|
)
|
||||||
|
.color(ui::Color::Custom(cx.theme().colors().text.opacity(0.8))),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.gap_1p5()
|
||||||
|
.child(illustration_element(false, 0.15))
|
||||||
|
.child(illustration_element(true, 0.3))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.pl_1()
|
||||||
|
.pr_2()
|
||||||
|
.py_0p5()
|
||||||
|
.gap_1()
|
||||||
|
.rounded_sm()
|
||||||
|
.bg(cx.theme().colors().element_active.opacity(0.2))
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors().border)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::AiGemini)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(Label::new("New Gemini CLI Thread").size(LabelSize::Small)),
|
||||||
|
)
|
||||||
|
.child(illustration_element(true, 0.3))
|
||||||
|
.child(illustration_element(false, 0.15)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let heading = v_flex()
|
||||||
|
.w_full()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Label::new("Now Available")
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(Headline::new("Bring Your Own Agent to Zed").size(HeadlineSize::Large));
|
||||||
|
|
||||||
|
let copy = "Bring the agent of your choice to Zed via our new Agent Client Protocol (ACP), starting with Google's Gemini CLI integration.";
|
||||||
|
|
||||||
|
let open_panel_button = Button::new("open-panel", "Start with Gemini CLI")
|
||||||
|
.icon_size(IconSize::Indicator)
|
||||||
|
.style(ButtonStyle::Tinted(TintColor::Accent))
|
||||||
|
.full_width()
|
||||||
|
.on_click(cx.listener(Self::open_panel));
|
||||||
|
|
||||||
|
let docs_button = Button::new("add-other-agents", "Add Other Agents")
|
||||||
|
.icon(IconName::ArrowUpRight)
|
||||||
|
.icon_size(IconSize::Indicator)
|
||||||
|
.icon_color(Color::Muted)
|
||||||
|
.full_width()
|
||||||
|
.on_click(cx.listener(Self::view_docs));
|
||||||
|
|
||||||
|
let close_button = h_flex().absolute().top_2().right_2().child(
|
||||||
|
IconButton::new("cancel", IconName::Close).on_click(cx.listener(
|
||||||
|
|_, _: &ClickEvent, _window, cx| {
|
||||||
|
acp_onboarding_event!("Canceled", trigger = "X click");
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
|
||||||
|
v_flex()
|
||||||
|
.id("acp-onboarding")
|
||||||
|
.key_context("AcpOnboardingModal")
|
||||||
|
.relative()
|
||||||
|
.w(rems(34.))
|
||||||
|
.h_full()
|
||||||
|
.elevation_3(cx)
|
||||||
|
.track_focus(&self.focus_handle(cx))
|
||||||
|
.overflow_hidden()
|
||||||
|
.on_action(cx.listener(Self::cancel))
|
||||||
|
.on_action(cx.listener(|_, _: &menu::Cancel, _window, cx| {
|
||||||
|
acp_onboarding_event!("Canceled", trigger = "Action");
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
}))
|
||||||
|
.on_any_mouse_down(cx.listener(|this, _: &MouseDownEvent, window, _cx| {
|
||||||
|
this.focus_handle.focus(window);
|
||||||
|
}))
|
||||||
|
.child(illustration)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.p_4()
|
||||||
|
.gap_2()
|
||||||
|
.child(heading)
|
||||||
|
.child(Label::new(copy).color(Color::Muted))
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.mt_2()
|
||||||
|
.gap_1()
|
||||||
|
.child(open_panel_button)
|
||||||
|
.child(docs_button),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(close_button)
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,3 +43,11 @@ pub fn ai_privacy_and_security(cx: &App) -> String {
|
||||||
server_url = server_url(cx)
|
server_url = server_url(cx)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the URL to Zed AI's external agents documentation.
|
||||||
|
pub fn external_agents_docs(cx: &App) -> String {
|
||||||
|
format!(
|
||||||
|
"{server_url}/docs/ai/external-agents",
|
||||||
|
server_url = server_url(cx)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -19,6 +19,10 @@ static KEYMAP_LINUX: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
load_keymap("keymaps/default-linux.json").expect("Failed to load Linux keymap")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
static KEYMAP_WINDOWS: LazyLock<KeymapFile> = LazyLock::new(|| {
|
||||||
|
load_keymap("keymaps/default-windows.json").expect("Failed to load Windows keymap")
|
||||||
|
});
|
||||||
|
|
||||||
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
static ALL_ACTIONS: LazyLock<Vec<ActionDef>> = LazyLock::new(dump_all_gpui_actions);
|
||||||
|
|
||||||
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
const FRONT_MATTER_COMMENT: &str = "<!-- ZED_META {} -->";
|
||||||
|
@ -216,6 +220,7 @@ fn find_binding(os: &str, action: &str) -> Option<String> {
|
||||||
let keymap = match os {
|
let keymap = match os {
|
||||||
"macos" => &KEYMAP_MACOS,
|
"macos" => &KEYMAP_MACOS,
|
||||||
"linux" | "freebsd" => &KEYMAP_LINUX,
|
"linux" | "freebsd" => &KEYMAP_LINUX,
|
||||||
|
"windows" => &KEYMAP_WINDOWS,
|
||||||
_ => unreachable!("Not a valid OS: {}", os),
|
_ => unreachable!("Not a valid OS: {}", os),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2588,7 +2588,7 @@ impl Editor {
|
||||||
|| binding
|
|| binding
|
||||||
.keystrokes()
|
.keystrokes()
|
||||||
.first()
|
.first()
|
||||||
.is_some_and(|keystroke| keystroke.modifiers.modified())
|
.is_some_and(|keystroke| keystroke.display_modifiers.modified())
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7686,16 +7686,16 @@ impl Editor {
|
||||||
.keystroke()
|
.keystroke()
|
||||||
{
|
{
|
||||||
modifiers_held = modifiers_held
|
modifiers_held = modifiers_held
|
||||||
|| (&accept_keystroke.modifiers == modifiers
|
|| (&accept_keystroke.display_modifiers == modifiers
|
||||||
&& accept_keystroke.modifiers.modified());
|
&& accept_keystroke.display_modifiers.modified());
|
||||||
};
|
};
|
||||||
if let Some(accept_partial_keystroke) = self
|
if let Some(accept_partial_keystroke) = self
|
||||||
.accept_edit_prediction_keybind(true, window, cx)
|
.accept_edit_prediction_keybind(true, window, cx)
|
||||||
.keystroke()
|
.keystroke()
|
||||||
{
|
{
|
||||||
modifiers_held = modifiers_held
|
modifiers_held = modifiers_held
|
||||||
|| (&accept_partial_keystroke.modifiers == modifiers
|
|| (&accept_partial_keystroke.display_modifiers == modifiers
|
||||||
&& accept_partial_keystroke.modifiers.modified());
|
&& accept_partial_keystroke.display_modifiers.modified());
|
||||||
}
|
}
|
||||||
|
|
||||||
if modifiers_held {
|
if modifiers_held {
|
||||||
|
@ -9044,7 +9044,7 @@ impl Editor {
|
||||||
|
|
||||||
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
let is_platform_style_mac = PlatformStyle::platform() == PlatformStyle::Mac;
|
||||||
|
|
||||||
let modifiers_color = if accept_keystroke.modifiers == window.modifiers() {
|
let modifiers_color = if accept_keystroke.display_modifiers == window.modifiers() {
|
||||||
Color::Accent
|
Color::Accent
|
||||||
} else {
|
} else {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
@ -9056,19 +9056,19 @@ impl Editor {
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.text_size(TextSize::XSmall.rems(cx))
|
.text_size(TextSize::XSmall.rems(cx))
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.modifiers,
|
&accept_keystroke.display_modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(modifiers_color),
|
Some(modifiers_color),
|
||||||
Some(IconSize::XSmall.rems().into()),
|
Some(IconSize::XSmall.rems().into()),
|
||||||
true,
|
true,
|
||||||
)))
|
)))
|
||||||
.when(is_platform_style_mac, |parent| {
|
.when(is_platform_style_mac, |parent| {
|
||||||
parent.child(accept_keystroke.key.clone())
|
parent.child(accept_keystroke.display_key.clone())
|
||||||
})
|
})
|
||||||
.when(!is_platform_style_mac, |parent| {
|
.when(!is_platform_style_mac, |parent| {
|
||||||
parent.child(
|
parent.child(
|
||||||
Key::new(
|
Key::new(
|
||||||
util::capitalize(&accept_keystroke.key),
|
util::capitalize(&accept_keystroke.display_key),
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
)
|
)
|
||||||
.size(Some(IconSize::XSmall.rems().into())),
|
.size(Some(IconSize::XSmall.rems().into())),
|
||||||
|
@ -9171,7 +9171,7 @@ impl Editor {
|
||||||
max_width: Pixels,
|
max_width: Pixels,
|
||||||
cursor_point: Point,
|
cursor_point: Point,
|
||||||
style: &EditorStyle,
|
style: &EditorStyle,
|
||||||
accept_keystroke: Option<&gpui::Keystroke>,
|
accept_keystroke: Option<&gpui::KeybindingKeystroke>,
|
||||||
_window: &Window,
|
_window: &Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) -> Option<AnyElement> {
|
) -> Option<AnyElement> {
|
||||||
|
@ -9249,7 +9249,7 @@ impl Editor {
|
||||||
accept_keystroke.as_ref(),
|
accept_keystroke.as_ref(),
|
||||||
|el, accept_keystroke| {
|
|el, accept_keystroke| {
|
||||||
el.child(h_flex().children(ui::render_modifiers(
|
el.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.modifiers,
|
&accept_keystroke.display_modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
Some(IconSize::XSmall.rems().into()),
|
Some(IconSize::XSmall.rems().into()),
|
||||||
|
@ -9319,7 +9319,7 @@ impl Editor {
|
||||||
.child(completion),
|
.child(completion),
|
||||||
)
|
)
|
||||||
.when_some(accept_keystroke, |el, accept_keystroke| {
|
.when_some(accept_keystroke, |el, accept_keystroke| {
|
||||||
if !accept_keystroke.modifiers.modified() {
|
if !accept_keystroke.display_modifiers.modified() {
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9338,7 +9338,7 @@ impl Editor {
|
||||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||||
.when(is_platform_style_mac, |parent| parent.gap_1())
|
.when(is_platform_style_mac, |parent| parent.gap_1())
|
||||||
.child(h_flex().children(ui::render_modifiers(
|
.child(h_flex().children(ui::render_modifiers(
|
||||||
&accept_keystroke.modifiers,
|
&accept_keystroke.display_modifiers,
|
||||||
PlatformStyle::platform(),
|
PlatformStyle::platform(),
|
||||||
Some(if !has_completion {
|
Some(if !has_completion {
|
||||||
Color::Muted
|
Color::Muted
|
||||||
|
|
|
@ -43,10 +43,10 @@ use gpui::{
|
||||||
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||||
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||||
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||||
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
KeybindingKeystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent,
|
||||||
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta,
|
||||||
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement,
|
||||||
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||||
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||||
transparent_black,
|
transparent_black,
|
||||||
};
|
};
|
||||||
|
@ -7150,7 +7150,7 @@ fn header_jump_data(
|
||||||
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
pub struct AcceptEditPredictionBinding(pub(crate) Option<gpui::KeyBinding>);
|
||||||
|
|
||||||
impl AcceptEditPredictionBinding {
|
impl AcceptEditPredictionBinding {
|
||||||
pub fn keystroke(&self) -> Option<&Keystroke> {
|
pub fn keystroke(&self) -> Option<&KeybindingKeystroke> {
|
||||||
if let Some(binding) = self.0.as_ref() {
|
if let Some(binding) = self.0.as_ref() {
|
||||||
match &binding.keystrokes() {
|
match &binding.keystrokes() {
|
||||||
[keystroke, ..] => Some(keystroke),
|
[keystroke, ..] => Some(keystroke),
|
||||||
|
|
|
@ -37,10 +37,10 @@ use crate::{
|
||||||
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
AssetSource, BackgroundExecutor, Bounds, ClipboardItem, CursorStyle, DispatchPhase, DisplayId,
|
||||||
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
EventEmitter, FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext,
|
||||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
Keymap, Keystroke, LayoutId, Menu, MenuItem, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, Point, PromptBuilder, PromptButton, PromptHandle,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, PromptBuilder,
|
||||||
PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource,
|
PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle,
|
||||||
SubscriberSet, Subscription, SvgRenderer, Task, TextSystem, Window, WindowAppearance,
|
Reservation, ScreenCaptureSource, SubscriberSet, Subscription, SvgRenderer, Task, TextSystem,
|
||||||
WindowHandle, WindowId, WindowInvalidator,
|
Window, WindowAppearance, WindowHandle, WindowId, WindowInvalidator,
|
||||||
colors::{Colors, GlobalColors},
|
colors::{Colors, GlobalColors},
|
||||||
current_platform, hash, init_app_menus,
|
current_platform, hash, init_app_menus,
|
||||||
};
|
};
|
||||||
|
@ -263,6 +263,7 @@ pub struct App {
|
||||||
pub(crate) focus_handles: Arc<FocusMap>,
|
pub(crate) focus_handles: Arc<FocusMap>,
|
||||||
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
pub(crate) keymap: Rc<RefCell<Keymap>>,
|
||||||
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
pub(crate) keyboard_layout: Box<dyn PlatformKeyboardLayout>,
|
||||||
|
pub(crate) keyboard_mapper: Rc<dyn PlatformKeyboardMapper>,
|
||||||
pub(crate) global_action_listeners:
|
pub(crate) global_action_listeners:
|
||||||
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
FxHashMap<TypeId, Vec<Rc<dyn Fn(&dyn Any, DispatchPhase, &mut Self)>>>,
|
||||||
pending_effects: VecDeque<Effect>,
|
pending_effects: VecDeque<Effect>,
|
||||||
|
@ -312,6 +313,7 @@ impl App {
|
||||||
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
let text_system = Arc::new(TextSystem::new(platform.text_system()));
|
||||||
let entities = EntityMap::new();
|
let entities = EntityMap::new();
|
||||||
let keyboard_layout = platform.keyboard_layout();
|
let keyboard_layout = platform.keyboard_layout();
|
||||||
|
let keyboard_mapper = platform.keyboard_mapper();
|
||||||
|
|
||||||
let app = Rc::new_cyclic(|this| AppCell {
|
let app = Rc::new_cyclic(|this| AppCell {
|
||||||
app: RefCell::new(App {
|
app: RefCell::new(App {
|
||||||
|
@ -337,6 +339,7 @@ impl App {
|
||||||
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
focus_handles: Arc::new(RwLock::new(SlotMap::with_key())),
|
||||||
keymap: Rc::new(RefCell::new(Keymap::default())),
|
keymap: Rc::new(RefCell::new(Keymap::default())),
|
||||||
keyboard_layout,
|
keyboard_layout,
|
||||||
|
keyboard_mapper,
|
||||||
global_action_listeners: FxHashMap::default(),
|
global_action_listeners: FxHashMap::default(),
|
||||||
pending_effects: VecDeque::new(),
|
pending_effects: VecDeque::new(),
|
||||||
pending_notifications: FxHashSet::default(),
|
pending_notifications: FxHashSet::default(),
|
||||||
|
@ -376,6 +379,7 @@ impl App {
|
||||||
if let Some(app) = app.upgrade() {
|
if let Some(app) = app.upgrade() {
|
||||||
let cx = &mut app.borrow_mut();
|
let cx = &mut app.borrow_mut();
|
||||||
cx.keyboard_layout = cx.platform.keyboard_layout();
|
cx.keyboard_layout = cx.platform.keyboard_layout();
|
||||||
|
cx.keyboard_mapper = cx.platform.keyboard_mapper();
|
||||||
cx.keyboard_layout_observers
|
cx.keyboard_layout_observers
|
||||||
.clone()
|
.clone()
|
||||||
.retain(&(), move |callback| (callback)(cx));
|
.retain(&(), move |callback| (callback)(cx));
|
||||||
|
@ -424,6 +428,11 @@ impl App {
|
||||||
self.keyboard_layout.as_ref()
|
self.keyboard_layout.as_ref()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the current keyboard mapper.
|
||||||
|
pub fn keyboard_mapper(&self) -> &Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
&self.keyboard_mapper
|
||||||
|
}
|
||||||
|
|
||||||
/// Invokes a handler when the current keyboard layout changes
|
/// Invokes a handler when the current keyboard layout changes
|
||||||
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
pub fn on_keyboard_layout_change<F>(&self, mut callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
|
|
|
@ -4,7 +4,7 @@ mod context;
|
||||||
pub use binding::*;
|
pub use binding::*;
|
||||||
pub use context::*;
|
pub use context::*;
|
||||||
|
|
||||||
use crate::{Action, Keystroke, is_no_action};
|
use crate::{Action, AsKeystroke, Keystroke, is_no_action};
|
||||||
use collections::{HashMap, HashSet};
|
use collections::{HashMap, HashSet};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
use std::any::TypeId;
|
use std::any::TypeId;
|
||||||
|
@ -141,7 +141,7 @@ impl Keymap {
|
||||||
/// only.
|
/// only.
|
||||||
pub fn bindings_for_input(
|
pub fn bindings_for_input(
|
||||||
&self,
|
&self,
|
||||||
input: &[Keystroke],
|
input: &[impl AsKeystroke],
|
||||||
context_stack: &[KeyContext],
|
context_stack: &[KeyContext],
|
||||||
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
) -> (SmallVec<[KeyBinding; 1]>, bool) {
|
||||||
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
let mut matched_bindings = SmallVec::<[(usize, BindingIndex, &KeyBinding); 1]>::new();
|
||||||
|
@ -192,7 +192,6 @@ impl Keymap {
|
||||||
|
|
||||||
(bindings, !pending.is_empty())
|
(bindings, !pending.is_empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given binding is enabled, given a certain key context.
|
/// Check if the given binding is enabled, given a certain key context.
|
||||||
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
/// Returns the deepest depth at which the binding matches, or None if it doesn't match.
|
||||||
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
fn binding_enabled(&self, binding: &KeyBinding, contexts: &[KeyContext]) -> Option<usize> {
|
||||||
|
@ -639,7 +638,7 @@ mod tests {
|
||||||
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
fn assert_bindings(keymap: &Keymap, action: &dyn Action, expected: &[&str]) {
|
||||||
let actual = keymap
|
let actual = keymap
|
||||||
.bindings_for_action(action)
|
.bindings_for_action(action)
|
||||||
.map(|binding| binding.keystrokes[0].unparse())
|
.map(|binding| binding.keystrokes[0].inner.unparse())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
assert_eq!(actual, expected, "{:?}", action);
|
assert_eq!(actual, expected, "{:?}", action);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use collections::HashMap;
|
use crate::{
|
||||||
|
Action, AsKeystroke, DummyKeyboardMapper, InvalidKeystrokeError, KeyBindingContextPredicate,
|
||||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
KeybindingKeystroke, Keystroke, PlatformKeyboardMapper, SharedString,
|
||||||
|
};
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
/// A keybinding and its associated metadata, from the keymap.
|
/// A keybinding and its associated metadata, from the keymap.
|
||||||
pub struct KeyBinding {
|
pub struct KeyBinding {
|
||||||
pub(crate) action: Box<dyn Action>,
|
pub(crate) action: Box<dyn Action>,
|
||||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
pub(crate) keystrokes: SmallVec<[KeybindingKeystroke; 2]>,
|
||||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||||
/// The json input string used when building the keybinding, if any
|
/// The json input string used when building the keybinding, if any
|
||||||
|
@ -32,7 +33,15 @@ impl KeyBinding {
|
||||||
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
pub fn new<A: Action>(keystrokes: &str, action: A, context: Option<&str>) -> Self {
|
||||||
let context_predicate =
|
let context_predicate =
|
||||||
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
context.map(|context| KeyBindingContextPredicate::parse(context).unwrap().into());
|
||||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
Self::load(
|
||||||
|
keystrokes,
|
||||||
|
Box::new(action),
|
||||||
|
context_predicate,
|
||||||
|
false,
|
||||||
|
None,
|
||||||
|
&DummyKeyboardMapper,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load a keybinding from the given raw data.
|
/// Load a keybinding from the given raw data.
|
||||||
|
@ -40,24 +49,22 @@ impl KeyBinding {
|
||||||
keystrokes: &str,
|
keystrokes: &str,
|
||||||
action: Box<dyn Action>,
|
action: Box<dyn Action>,
|
||||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
key_equivalents: Option<&HashMap<char, char>>,
|
use_key_equivalents: bool,
|
||||||
action_input: Option<SharedString>,
|
action_input: Option<SharedString>,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
let keystrokes: SmallVec<[KeybindingKeystroke; 2]> = keystrokes
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(Keystroke::parse)
|
.map(|source| {
|
||||||
|
let keystroke = Keystroke::parse(source)?;
|
||||||
|
Ok(KeybindingKeystroke::new(
|
||||||
|
keystroke,
|
||||||
|
use_key_equivalents,
|
||||||
|
keyboard_mapper,
|
||||||
|
))
|
||||||
|
})
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
|
||||||
if let Some(equivalents) = key_equivalents {
|
|
||||||
for keystroke in keystrokes.iter_mut() {
|
|
||||||
if keystroke.key.chars().count() == 1
|
|
||||||
&& let Some(key) = equivalents.get(&keystroke.key.chars().next().unwrap())
|
|
||||||
{
|
|
||||||
keystroke.key = key.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
|
@ -79,13 +86,13 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if the given keystrokes match this binding.
|
/// Check if the given keystrokes match this binding.
|
||||||
pub fn match_keystrokes(&self, typed: &[Keystroke]) -> Option<bool> {
|
pub fn match_keystrokes(&self, typed: &[impl AsKeystroke]) -> Option<bool> {
|
||||||
if self.keystrokes.len() < typed.len() {
|
if self.keystrokes.len() < typed.len() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
for (target, typed) in self.keystrokes.iter().zip(typed.iter()) {
|
||||||
if !typed.should_match(target) {
|
if !typed.as_keystroke().should_match(target) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -94,7 +101,7 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the keystrokes associated with this binding
|
/// Get the keystrokes associated with this binding
|
||||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||||
self.keystrokes.as_slice()
|
self.keystrokes.as_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -231,7 +231,6 @@ pub(crate) trait Platform: 'static {
|
||||||
|
|
||||||
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
fn on_quit(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
fn on_reopen(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
|
||||||
|
|
||||||
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
fn set_menus(&self, menus: Vec<Menu>, keymap: &Keymap);
|
||||||
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
fn get_menus(&self) -> Option<Vec<OwnedMenu>> {
|
||||||
|
@ -251,7 +250,6 @@ pub(crate) trait Platform: 'static {
|
||||||
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
fn on_app_menu_action(&self, callback: Box<dyn FnMut(&dyn Action)>);
|
||||||
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
fn on_will_open_app_menu(&self, callback: Box<dyn FnMut()>);
|
||||||
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
fn on_validate_app_menu_command(&self, callback: Box<dyn FnMut(&dyn Action) -> bool>);
|
||||||
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
|
||||||
|
|
||||||
fn compositor_name(&self) -> &'static str {
|
fn compositor_name(&self) -> &'static str {
|
||||||
""
|
""
|
||||||
|
@ -272,6 +270,10 @@ pub(crate) trait Platform: 'static {
|
||||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>>;
|
||||||
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
fn read_credentials(&self, url: &str) -> Task<Result<Option<(String, Vec<u8>)>>>;
|
||||||
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
fn delete_credentials(&self, url: &str) -> Task<Result<()>>;
|
||||||
|
|
||||||
|
fn keyboard_layout(&self) -> Box<dyn PlatformKeyboardLayout>;
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper>;
|
||||||
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
/// A handle to a platform's display, e.g. a monitor or laptop screen.
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
use collections::HashMap;
|
||||||
|
|
||||||
|
use crate::{KeybindingKeystroke, Keystroke};
|
||||||
|
|
||||||
/// A trait for platform-specific keyboard layouts
|
/// A trait for platform-specific keyboard layouts
|
||||||
pub trait PlatformKeyboardLayout {
|
pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout ID, which should be unique to the layout
|
/// Get the keyboard layout ID, which should be unique to the layout
|
||||||
|
@ -5,3 +9,33 @@ pub trait PlatformKeyboardLayout {
|
||||||
/// Get the keyboard layout display name
|
/// Get the keyboard layout display name
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A trait for platform-specific keyboard mappings
|
||||||
|
pub trait PlatformKeyboardMapper {
|
||||||
|
/// Map a key equivalent to its platform-specific representation
|
||||||
|
fn map_key_equivalent(
|
||||||
|
&self,
|
||||||
|
keystroke: Keystroke,
|
||||||
|
use_key_equivalents: bool,
|
||||||
|
) -> KeybindingKeystroke;
|
||||||
|
/// Get the key equivalents for the current keyboard layout,
|
||||||
|
/// only used on macOS
|
||||||
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dummy implementation of the platform keyboard mapper
|
||||||
|
pub struct DummyKeyboardMapper;
|
||||||
|
|
||||||
|
impl PlatformKeyboardMapper for DummyKeyboardMapper {
|
||||||
|
fn map_key_equivalent(
|
||||||
|
&self,
|
||||||
|
keystroke: Keystroke,
|
||||||
|
_use_key_equivalents: bool,
|
||||||
|
) -> KeybindingKeystroke {
|
||||||
|
KeybindingKeystroke::from_keystroke(keystroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -5,6 +5,14 @@ use std::{
|
||||||
fmt::{Display, Write},
|
fmt::{Display, Write},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::PlatformKeyboardMapper;
|
||||||
|
|
||||||
|
/// This is a helper trait so that we can simplify the implementation of some functions
|
||||||
|
pub trait AsKeystroke {
|
||||||
|
/// Returns the GPUI representation of the keystroke.
|
||||||
|
fn as_keystroke(&self) -> &Keystroke;
|
||||||
|
}
|
||||||
|
|
||||||
/// A keystroke and associated metadata generated by the platform
|
/// A keystroke and associated metadata generated by the platform
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Default, Deserialize, Hash)]
|
||||||
pub struct Keystroke {
|
pub struct Keystroke {
|
||||||
|
@ -24,6 +32,17 @@ pub struct Keystroke {
|
||||||
pub key_char: Option<String>,
|
pub key_char: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Represents a keystroke that can be used in keybindings and displayed to the user.
|
||||||
|
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct KeybindingKeystroke {
|
||||||
|
/// The GPUI representation of the keystroke.
|
||||||
|
pub inner: Keystroke,
|
||||||
|
/// The modifiers to display.
|
||||||
|
pub display_modifiers: Modifiers,
|
||||||
|
/// The key to display.
|
||||||
|
pub display_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
/// Error type for `Keystroke::parse`. This is used instead of `anyhow::Error` so that Zed can use
|
||||||
/// markdown to display it.
|
/// markdown to display it.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -58,7 +77,7 @@ impl Keystroke {
|
||||||
///
|
///
|
||||||
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
/// This method assumes that `self` was typed and `target' is in the keymap, and checks
|
||||||
/// both possibilities for self against the target.
|
/// both possibilities for self against the target.
|
||||||
pub fn should_match(&self, target: &Keystroke) -> bool {
|
pub fn should_match(&self, target: &KeybindingKeystroke) -> bool {
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
if let Some(key_char) = self
|
if let Some(key_char) = self
|
||||||
.key_char
|
.key_char
|
||||||
|
@ -71,7 +90,7 @@ impl Keystroke {
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
|
|
||||||
if &target.key == key_char && target.modifiers == ime_modifiers {
|
if &target.inner.key == key_char && target.inner.modifiers == ime_modifiers {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -83,12 +102,12 @@ impl Keystroke {
|
||||||
.filter(|key_char| key_char != &&self.key)
|
.filter(|key_char| key_char != &&self.key)
|
||||||
{
|
{
|
||||||
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
// On Windows, if key_char is set, then the typed keystroke produced the key_char
|
||||||
if &target.key == key_char && target.modifiers == Modifiers::none() {
|
if &target.inner.key == key_char && target.inner.modifiers == Modifiers::none() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
target.modifiers == self.modifiers && target.key == self.key
|
target.inner.modifiers == self.modifiers && target.inner.key == self.key
|
||||||
}
|
}
|
||||||
|
|
||||||
/// key syntax is:
|
/// key syntax is:
|
||||||
|
@ -200,31 +219,7 @@ impl Keystroke {
|
||||||
|
|
||||||
/// Produces a representation of this key that Parse can understand.
|
/// Produces a representation of this key that Parse can understand.
|
||||||
pub fn unparse(&self) -> String {
|
pub fn unparse(&self) -> String {
|
||||||
let mut str = String::new();
|
unparse(&self.modifiers, &self.key)
|
||||||
if self.modifiers.function {
|
|
||||||
str.push_str("fn-");
|
|
||||||
}
|
|
||||||
if self.modifiers.control {
|
|
||||||
str.push_str("ctrl-");
|
|
||||||
}
|
|
||||||
if self.modifiers.alt {
|
|
||||||
str.push_str("alt-");
|
|
||||||
}
|
|
||||||
if self.modifiers.platform {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
str.push_str("cmd-");
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
||||||
str.push_str("super-");
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
str.push_str("win-");
|
|
||||||
}
|
|
||||||
if self.modifiers.shift {
|
|
||||||
str.push_str("shift-");
|
|
||||||
}
|
|
||||||
str.push_str(&self.key);
|
|
||||||
str
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this keystroke left
|
/// Returns true if this keystroke left
|
||||||
|
@ -266,6 +261,32 @@ impl Keystroke {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl KeybindingKeystroke {
|
||||||
|
/// Create a new keybinding keystroke from the given keystroke
|
||||||
|
pub fn new(
|
||||||
|
inner: Keystroke,
|
||||||
|
use_key_equivalents: bool,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
|
) -> Self {
|
||||||
|
keyboard_mapper.map_key_equivalent(inner, use_key_equivalents)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn from_keystroke(keystroke: Keystroke) -> Self {
|
||||||
|
let key = keystroke.key.clone();
|
||||||
|
let modifiers = keystroke.modifiers;
|
||||||
|
KeybindingKeystroke {
|
||||||
|
inner: keystroke,
|
||||||
|
display_modifiers: modifiers,
|
||||||
|
display_key: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Produces a representation of this key that Parse can understand.
|
||||||
|
pub fn unparse(&self) -> String {
|
||||||
|
unparse(&self.display_modifiers, &self.display_key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn is_printable_key(key: &str) -> bool {
|
fn is_printable_key(key: &str) -> bool {
|
||||||
!matches!(
|
!matches!(
|
||||||
key,
|
key,
|
||||||
|
@ -322,65 +343,15 @@ fn is_printable_key(key: &str) -> bool {
|
||||||
|
|
||||||
impl std::fmt::Display for Keystroke {
|
impl std::fmt::Display for Keystroke {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
if self.modifiers.control {
|
display_modifiers(&self.modifiers, f)?;
|
||||||
#[cfg(target_os = "macos")]
|
display_key(&self.key, f)
|
||||||
f.write_char('^')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "ctrl-")?;
|
|
||||||
}
|
}
|
||||||
if self.modifiers.alt {
|
}
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⌥')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
impl std::fmt::Display for KeybindingKeystroke {
|
||||||
write!(f, "alt-")?;
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
}
|
display_modifiers(&self.display_modifiers, f)?;
|
||||||
if self.modifiers.platform {
|
display_key(&self.display_key, f)
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⌘')?;
|
|
||||||
|
|
||||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
|
||||||
f.write_char('❖')?;
|
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
f.write_char('⊞')?;
|
|
||||||
}
|
|
||||||
if self.modifiers.shift {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
f.write_char('⇧')?;
|
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
|
||||||
write!(f, "shift-")?;
|
|
||||||
}
|
|
||||||
let key = match self.key.as_str() {
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"backspace" => '⌫',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"up" => '↑',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"down" => '↓',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"left" => '←',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"right" => '→',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"tab" => '⇥',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"escape" => '⎋',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"shift" => '⇧',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"control" => '⌃',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"alt" => '⌥',
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
"platform" => '⌘',
|
|
||||||
|
|
||||||
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
|
||||||
key => return f.write_str(key),
|
|
||||||
};
|
|
||||||
f.write_char(key)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -600,3 +571,110 @@ pub struct Capslock {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub on: bool,
|
pub on: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AsKeystroke for Keystroke {
|
||||||
|
fn as_keystroke(&self) -> &Keystroke {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AsKeystroke for KeybindingKeystroke {
|
||||||
|
fn as_keystroke(&self) -> &Keystroke {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_modifiers(modifiers: &Modifiers, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
if modifiers.control {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('^')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "ctrl-")?;
|
||||||
|
}
|
||||||
|
if modifiers.alt {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌥')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "alt-")?;
|
||||||
|
}
|
||||||
|
if modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⌘')?;
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
f.write_char('❖')?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
f.write_char('⊞')?;
|
||||||
|
}
|
||||||
|
if modifiers.shift {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
f.write_char('⇧')?;
|
||||||
|
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
write!(f, "shift-")?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn display_key(key: &str, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let key = match key {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"backspace" => '⌫',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"up" => '↑',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"down" => '↓',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"left" => '←',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"right" => '→',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"tab" => '⇥',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"escape" => '⎋',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"shift" => '⇧',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"control" => '⌃',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"alt" => '⌥',
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
"platform" => '⌘',
|
||||||
|
|
||||||
|
key if key.len() == 1 => key.chars().next().unwrap().to_ascii_uppercase(),
|
||||||
|
key => return f.write_str(key),
|
||||||
|
};
|
||||||
|
f.write_char(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn unparse(modifiers: &Modifiers, key: &str) -> String {
|
||||||
|
let mut result = String::new();
|
||||||
|
if modifiers.function {
|
||||||
|
result.push_str("fn-");
|
||||||
|
}
|
||||||
|
if modifiers.control {
|
||||||
|
result.push_str("ctrl-");
|
||||||
|
}
|
||||||
|
if modifiers.alt {
|
||||||
|
result.push_str("alt-");
|
||||||
|
}
|
||||||
|
if modifiers.platform {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
result.push_str("cmd-");
|
||||||
|
|
||||||
|
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||||
|
result.push_str("super-");
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
result.push_str("win-");
|
||||||
|
}
|
||||||
|
if modifiers.shift {
|
||||||
|
result.push_str("shift-");
|
||||||
|
}
|
||||||
|
result.push_str(&key);
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
|
@ -25,8 +25,8 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||||
use crate::{
|
use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DisplayId,
|
||||||
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
ForegroundExecutor, Keymap, LinuxDispatcher, Menu, MenuItem, OwnedMenu, PathPromptOptions,
|
||||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow,
|
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
Point, Result, Task, WindowAppearance, WindowParams, px,
|
PlatformTextSystem, PlatformWindow, Point, Result, Task, WindowAppearance, WindowParams, px,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||||
|
@ -144,6 +144,10 @@ impl<P: LinuxClient + 'static> Platform for P {
|
||||||
self.keyboard_layout()
|
self.keyboard_layout()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
Rc::new(crate::DummyKeyboardMapper)
|
||||||
|
}
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||||
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
self.with_common(|common| common.callbacks.keyboard_layout_change = Some(callback));
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
||||||
use super::{
|
use super::{
|
||||||
BoolExt, MacKeyboardLayout,
|
BoolExt, MacKeyboardLayout, MacKeyboardMapper,
|
||||||
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
attributed_string::{NSAttributedString, NSMutableAttributedString},
|
||||||
events::key_to_native,
|
events::key_to_native,
|
||||||
renderer,
|
renderer,
|
||||||
|
@ -8,8 +8,9 @@ use crate::{
|
||||||
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
Action, AnyWindowHandle, BackgroundExecutor, ClipboardEntry, ClipboardItem, ClipboardString,
|
||||||
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
CursorStyle, ForegroundExecutor, Image, ImageFormat, KeyContext, Keymap, MacDispatcher,
|
||||||
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
MacDisplay, MacWindow, Menu, MenuItem, OsMenu, OwnedMenu, PathPromptOptions, Platform,
|
||||||
PlatformDisplay, PlatformKeyboardLayout, PlatformTextSystem, PlatformWindow, Result,
|
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||||
SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams, hash,
|
PlatformWindow, Result, SemanticVersion, SystemMenuType, Task, WindowAppearance, WindowParams,
|
||||||
|
hash,
|
||||||
};
|
};
|
||||||
use anyhow::{Context as _, anyhow};
|
use anyhow::{Context as _, anyhow};
|
||||||
use block::ConcreteBlock;
|
use block::ConcreteBlock;
|
||||||
|
@ -171,6 +172,7 @@ pub(crate) struct MacPlatformState {
|
||||||
finish_launching: Option<Box<dyn FnOnce()>>,
|
finish_launching: Option<Box<dyn FnOnce()>>,
|
||||||
dock_menu: Option<id>,
|
dock_menu: Option<id>,
|
||||||
menus: Option<Vec<OwnedMenu>>,
|
menus: Option<Vec<OwnedMenu>>,
|
||||||
|
keyboard_mapper: Rc<MacKeyboardMapper>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MacPlatform {
|
impl Default for MacPlatform {
|
||||||
|
@ -189,6 +191,9 @@ impl MacPlatform {
|
||||||
#[cfg(not(feature = "font-kit"))]
|
#[cfg(not(feature = "font-kit"))]
|
||||||
let text_system = Arc::new(crate::NoopTextSystem::new());
|
let text_system = Arc::new(crate::NoopTextSystem::new());
|
||||||
|
|
||||||
|
let keyboard_layout = MacKeyboardLayout::new();
|
||||||
|
let keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||||
|
|
||||||
Self(Mutex::new(MacPlatformState {
|
Self(Mutex::new(MacPlatformState {
|
||||||
headless,
|
headless,
|
||||||
text_system,
|
text_system,
|
||||||
|
@ -209,6 +214,7 @@ impl MacPlatform {
|
||||||
dock_menu: None,
|
dock_menu: None,
|
||||||
on_keyboard_layout_change: None,
|
on_keyboard_layout_change: None,
|
||||||
menus: None,
|
menus: None,
|
||||||
|
keyboard_mapper,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,19 +354,19 @@ impl MacPlatform {
|
||||||
let mut mask = NSEventModifierFlags::empty();
|
let mut mask = NSEventModifierFlags::empty();
|
||||||
for (modifier, flag) in &[
|
for (modifier, flag) in &[
|
||||||
(
|
(
|
||||||
keystroke.modifiers.platform,
|
keystroke.display_modifiers.platform,
|
||||||
NSEventModifierFlags::NSCommandKeyMask,
|
NSEventModifierFlags::NSCommandKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.modifiers.control,
|
keystroke.display_modifiers.control,
|
||||||
NSEventModifierFlags::NSControlKeyMask,
|
NSEventModifierFlags::NSControlKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.modifiers.alt,
|
keystroke.display_modifiers.alt,
|
||||||
NSEventModifierFlags::NSAlternateKeyMask,
|
NSEventModifierFlags::NSAlternateKeyMask,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
keystroke.modifiers.shift,
|
keystroke.display_modifiers.shift,
|
||||||
NSEventModifierFlags::NSShiftKeyMask,
|
NSEventModifierFlags::NSShiftKeyMask,
|
||||||
),
|
),
|
||||||
] {
|
] {
|
||||||
|
@ -373,7 +379,7 @@ impl MacPlatform {
|
||||||
.initWithTitle_action_keyEquivalent_(
|
.initWithTitle_action_keyEquivalent_(
|
||||||
ns_string(name),
|
ns_string(name),
|
||||||
selector,
|
selector,
|
||||||
ns_string(key_to_native(&keystroke.key).as_ref()),
|
ns_string(key_to_native(&keystroke.display_key).as_ref()),
|
||||||
)
|
)
|
||||||
.autorelease();
|
.autorelease();
|
||||||
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
if Self::os_version() >= SemanticVersion::new(12, 0, 0) {
|
||||||
|
@ -882,6 +888,10 @@ impl Platform for MacPlatform {
|
||||||
Box::new(MacKeyboardLayout::new())
|
Box::new(MacKeyboardLayout::new())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
self.0.lock().keyboard_mapper.clone()
|
||||||
|
}
|
||||||
|
|
||||||
fn app_path(&self) -> Result<PathBuf> {
|
fn app_path(&self) -> Result<PathBuf> {
|
||||||
unsafe {
|
unsafe {
|
||||||
let bundle: id = NSBundle::mainBundle();
|
let bundle: id = NSBundle::mainBundle();
|
||||||
|
@ -1393,6 +1403,8 @@ extern "C" fn will_terminate(this: &mut Object, _: Sel, _: id) {
|
||||||
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
extern "C" fn on_keyboard_layout_change(this: &mut Object, _: Sel, _: id) {
|
||||||
let platform = unsafe { get_mac_platform(this) };
|
let platform = unsafe { get_mac_platform(this) };
|
||||||
let mut lock = platform.0.lock();
|
let mut lock = platform.0.lock();
|
||||||
|
let keyboard_layout = MacKeyboardLayout::new();
|
||||||
|
lock.keyboard_mapper = Rc::new(MacKeyboardMapper::new(keyboard_layout.id()));
|
||||||
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
if let Some(mut callback) = lock.on_keyboard_layout_change.take() {
|
||||||
drop(lock);
|
drop(lock);
|
||||||
callback();
|
callback();
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay, PlatformKeyboardLayout,
|
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
||||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, PromptButton,
|
||||||
SourceMetadata, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata, Task,
|
||||||
|
TestDisplay, TestWindow, WindowAppearance, WindowParams, size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
@ -237,6 +238,10 @@ impl Platform for TestPlatform {
|
||||||
Box::new(TestKeyboardLayout)
|
Box::new(TestKeyboardLayout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
Rc::new(DummyKeyboardMapper)
|
||||||
|
}
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
fn on_keyboard_layout_change(&self, _: Box<dyn FnMut()>) {}
|
||||||
|
|
||||||
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
fn run(&self, _on_finish_launching: Box<dyn FnOnce()>) {
|
||||||
|
|
|
@ -1,22 +1,31 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use collections::HashMap;
|
||||||
use windows::Win32::UI::{
|
use windows::Win32::UI::{
|
||||||
Input::KeyboardAndMouse::{
|
Input::KeyboardAndMouse::{
|
||||||
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MapVirtualKeyW, ToUnicode, VIRTUAL_KEY, VK_0,
|
GetKeyboardLayoutNameW, MAPVK_VK_TO_CHAR, MAPVK_VK_TO_VSC, MapVirtualKeyW, ToUnicode,
|
||||||
VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1, VK_CONTROL, VK_MENU,
|
VIRTUAL_KEY, VK_0, VK_1, VK_2, VK_3, VK_4, VK_5, VK_6, VK_7, VK_8, VK_9, VK_ABNT_C1,
|
||||||
VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7, VK_OEM_8, VK_OEM_102,
|
VK_CONTROL, VK_MENU, VK_OEM_1, VK_OEM_2, VK_OEM_3, VK_OEM_4, VK_OEM_5, VK_OEM_6, VK_OEM_7,
|
||||||
VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
VK_OEM_8, VK_OEM_102, VK_OEM_COMMA, VK_OEM_MINUS, VK_OEM_PERIOD, VK_OEM_PLUS, VK_SHIFT,
|
||||||
},
|
},
|
||||||
WindowsAndMessaging::KL_NAMELENGTH,
|
WindowsAndMessaging::KL_NAMELENGTH,
|
||||||
};
|
};
|
||||||
use windows_core::HSTRING;
|
use windows_core::HSTRING;
|
||||||
|
|
||||||
use crate::{Modifiers, PlatformKeyboardLayout};
|
use crate::{
|
||||||
|
KeybindingKeystroke, Keystroke, Modifiers, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) struct WindowsKeyboardLayout {
|
pub(crate) struct WindowsKeyboardLayout {
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct WindowsKeyboardMapper {
|
||||||
|
key_to_vkey: HashMap<String, (u16, bool)>,
|
||||||
|
vkey_to_key: HashMap<u16, String>,
|
||||||
|
vkey_to_shifted: HashMap<u16, String>,
|
||||||
|
}
|
||||||
|
|
||||||
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||||
fn id(&self) -> &str {
|
fn id(&self) -> &str {
|
||||||
&self.id
|
&self.id
|
||||||
|
@ -27,6 +36,65 @@ impl PlatformKeyboardLayout for WindowsKeyboardLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PlatformKeyboardMapper for WindowsKeyboardMapper {
|
||||||
|
fn map_key_equivalent(
|
||||||
|
&self,
|
||||||
|
mut keystroke: Keystroke,
|
||||||
|
use_key_equivalents: bool,
|
||||||
|
) -> KeybindingKeystroke {
|
||||||
|
let Some((vkey, shifted_key)) = self.get_vkey_from_key(&keystroke.key, use_key_equivalents)
|
||||||
|
else {
|
||||||
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||||
|
};
|
||||||
|
if shifted_key && keystroke.modifiers.shift {
|
||||||
|
log::warn!(
|
||||||
|
"Keystroke '{}' has both shift and a shifted key, this is likely a bug",
|
||||||
|
keystroke.key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shift = shifted_key || keystroke.modifiers.shift;
|
||||||
|
keystroke.modifiers.shift = false;
|
||||||
|
|
||||||
|
let Some(key) = self.vkey_to_key.get(&vkey).cloned() else {
|
||||||
|
log::error!(
|
||||||
|
"Failed to map key equivalent '{:?}' to a valid key",
|
||||||
|
keystroke
|
||||||
|
);
|
||||||
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||||
|
};
|
||||||
|
|
||||||
|
keystroke.key = if shift {
|
||||||
|
let Some(shifted_key) = self.vkey_to_shifted.get(&vkey).cloned() else {
|
||||||
|
log::error!(
|
||||||
|
"Failed to map keystroke {:?} with virtual key '{:?}' to a shifted key",
|
||||||
|
keystroke,
|
||||||
|
vkey
|
||||||
|
);
|
||||||
|
return KeybindingKeystroke::from_keystroke(keystroke);
|
||||||
|
};
|
||||||
|
shifted_key
|
||||||
|
} else {
|
||||||
|
key.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let modifiers = Modifiers {
|
||||||
|
shift,
|
||||||
|
..keystroke.modifiers
|
||||||
|
};
|
||||||
|
|
||||||
|
KeybindingKeystroke {
|
||||||
|
inner: keystroke,
|
||||||
|
display_modifiers: modifiers,
|
||||||
|
display_key: key,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_key_equivalents(&self) -> Option<&HashMap<char, char>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl WindowsKeyboardLayout {
|
impl WindowsKeyboardLayout {
|
||||||
pub(crate) fn new() -> Result<Self> {
|
pub(crate) fn new() -> Result<Self> {
|
||||||
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
let mut buffer = [0u16; KL_NAMELENGTH as usize];
|
||||||
|
@ -48,6 +116,41 @@ impl WindowsKeyboardLayout {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl WindowsKeyboardMapper {
|
||||||
|
pub(crate) fn new() -> Self {
|
||||||
|
let mut key_to_vkey = HashMap::default();
|
||||||
|
let mut vkey_to_key = HashMap::default();
|
||||||
|
let mut vkey_to_shifted = HashMap::default();
|
||||||
|
for vkey in CANDIDATE_VKEYS {
|
||||||
|
if let Some(key) = get_key_from_vkey(*vkey) {
|
||||||
|
key_to_vkey.insert(key.clone(), (vkey.0, false));
|
||||||
|
vkey_to_key.insert(vkey.0, key);
|
||||||
|
}
|
||||||
|
let scan_code = unsafe { MapVirtualKeyW(vkey.0 as u32, MAPVK_VK_TO_VSC) };
|
||||||
|
if scan_code == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Some(shifted_key) = get_shifted_key(*vkey, scan_code) {
|
||||||
|
key_to_vkey.insert(shifted_key.clone(), (vkey.0, true));
|
||||||
|
vkey_to_shifted.insert(vkey.0, shifted_key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self {
|
||||||
|
key_to_vkey,
|
||||||
|
vkey_to_key,
|
||||||
|
vkey_to_shifted,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_vkey_from_key(&self, key: &str, use_key_equivalents: bool) -> Option<(u16, bool)> {
|
||||||
|
if use_key_equivalents {
|
||||||
|
get_vkey_from_key_with_us_layout(key)
|
||||||
|
} else {
|
||||||
|
self.key_to_vkey.get(key).cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn get_keystroke_key(
|
pub(crate) fn get_keystroke_key(
|
||||||
vkey: VIRTUAL_KEY,
|
vkey: VIRTUAL_KEY,
|
||||||
scan_code: u32,
|
scan_code: u32,
|
||||||
|
@ -140,3 +243,134 @@ pub(crate) fn generate_key_char(
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_vkey_from_key_with_us_layout(key: &str) -> Option<(u16, bool)> {
|
||||||
|
match key {
|
||||||
|
// ` => VK_OEM_3
|
||||||
|
"`" => Some((VK_OEM_3.0, false)),
|
||||||
|
"~" => Some((VK_OEM_3.0, true)),
|
||||||
|
"1" => Some((VK_1.0, false)),
|
||||||
|
"!" => Some((VK_1.0, true)),
|
||||||
|
"2" => Some((VK_2.0, false)),
|
||||||
|
"@" => Some((VK_2.0, true)),
|
||||||
|
"3" => Some((VK_3.0, false)),
|
||||||
|
"#" => Some((VK_3.0, true)),
|
||||||
|
"4" => Some((VK_4.0, false)),
|
||||||
|
"$" => Some((VK_4.0, true)),
|
||||||
|
"5" => Some((VK_5.0, false)),
|
||||||
|
"%" => Some((VK_5.0, true)),
|
||||||
|
"6" => Some((VK_6.0, false)),
|
||||||
|
"^" => Some((VK_6.0, true)),
|
||||||
|
"7" => Some((VK_7.0, false)),
|
||||||
|
"&" => Some((VK_7.0, true)),
|
||||||
|
"8" => Some((VK_8.0, false)),
|
||||||
|
"*" => Some((VK_8.0, true)),
|
||||||
|
"9" => Some((VK_9.0, false)),
|
||||||
|
"(" => Some((VK_9.0, true)),
|
||||||
|
"0" => Some((VK_0.0, false)),
|
||||||
|
")" => Some((VK_0.0, true)),
|
||||||
|
"-" => Some((VK_OEM_MINUS.0, false)),
|
||||||
|
"_" => Some((VK_OEM_MINUS.0, true)),
|
||||||
|
"=" => Some((VK_OEM_PLUS.0, false)),
|
||||||
|
"+" => Some((VK_OEM_PLUS.0, true)),
|
||||||
|
"[" => Some((VK_OEM_4.0, false)),
|
||||||
|
"{" => Some((VK_OEM_4.0, true)),
|
||||||
|
"]" => Some((VK_OEM_6.0, false)),
|
||||||
|
"}" => Some((VK_OEM_6.0, true)),
|
||||||
|
"\\" => Some((VK_OEM_5.0, false)),
|
||||||
|
"|" => Some((VK_OEM_5.0, true)),
|
||||||
|
";" => Some((VK_OEM_1.0, false)),
|
||||||
|
":" => Some((VK_OEM_1.0, true)),
|
||||||
|
"'" => Some((VK_OEM_7.0, false)),
|
||||||
|
"\"" => Some((VK_OEM_7.0, true)),
|
||||||
|
"," => Some((VK_OEM_COMMA.0, false)),
|
||||||
|
"<" => Some((VK_OEM_COMMA.0, true)),
|
||||||
|
"." => Some((VK_OEM_PERIOD.0, false)),
|
||||||
|
">" => Some((VK_OEM_PERIOD.0, true)),
|
||||||
|
"/" => Some((VK_OEM_2.0, false)),
|
||||||
|
"?" => Some((VK_OEM_2.0, true)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const CANDIDATE_VKEYS: &[VIRTUAL_KEY] = &[
|
||||||
|
VK_OEM_3,
|
||||||
|
VK_OEM_MINUS,
|
||||||
|
VK_OEM_PLUS,
|
||||||
|
VK_OEM_4,
|
||||||
|
VK_OEM_5,
|
||||||
|
VK_OEM_6,
|
||||||
|
VK_OEM_1,
|
||||||
|
VK_OEM_7,
|
||||||
|
VK_OEM_COMMA,
|
||||||
|
VK_OEM_PERIOD,
|
||||||
|
VK_OEM_2,
|
||||||
|
VK_OEM_102,
|
||||||
|
VK_OEM_8,
|
||||||
|
VK_ABNT_C1,
|
||||||
|
VK_0,
|
||||||
|
VK_1,
|
||||||
|
VK_2,
|
||||||
|
VK_3,
|
||||||
|
VK_4,
|
||||||
|
VK_5,
|
||||||
|
VK_6,
|
||||||
|
VK_7,
|
||||||
|
VK_8,
|
||||||
|
VK_9,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{Keystroke, Modifiers, PlatformKeyboardMapper, WindowsKeyboardMapper};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_keyboard_mapper() {
|
||||||
|
let mapper = WindowsKeyboardMapper::new();
|
||||||
|
|
||||||
|
// Normal case
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control(),
|
||||||
|
key: "a".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||||
|
assert_eq!(mapped.inner, keystroke);
|
||||||
|
assert_eq!(mapped.display_key, "a");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control());
|
||||||
|
|
||||||
|
// Shifted case, ctrl-$
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control(),
|
||||||
|
key: "$".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke.clone(), true);
|
||||||
|
assert_eq!(mapped.inner, keystroke);
|
||||||
|
assert_eq!(mapped.display_key, "4");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||||
|
|
||||||
|
// Shifted case, but shift is true
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control_shift(),
|
||||||
|
key: "$".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||||
|
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||||
|
assert_eq!(mapped.display_key, "4");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||||
|
|
||||||
|
// Windows style
|
||||||
|
let keystroke = Keystroke {
|
||||||
|
modifiers: Modifiers::control_shift(),
|
||||||
|
key: "4".to_string(),
|
||||||
|
key_char: None,
|
||||||
|
};
|
||||||
|
let mapped = mapper.map_key_equivalent(keystroke, true);
|
||||||
|
assert_eq!(mapped.inner.modifiers, Modifiers::control());
|
||||||
|
assert_eq!(mapped.inner.key, "$");
|
||||||
|
assert_eq!(mapped.display_key, "4");
|
||||||
|
assert_eq!(mapped.display_modifiers, Modifiers::control_shift());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -351,6 +351,10 @@ impl Platform for WindowsPlatform {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn keyboard_mapper(&self) -> Rc<dyn PlatformKeyboardMapper> {
|
||||||
|
Rc::new(WindowsKeyboardMapper::new())
|
||||||
|
}
|
||||||
|
|
||||||
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
fn on_keyboard_layout_change(&self, callback: Box<dyn FnMut()>) {
|
||||||
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
self.state.borrow_mut().callbacks.keyboard_layout_change = Some(callback);
|
||||||
}
|
}
|
||||||
|
|
|
@ -215,6 +215,7 @@ pub enum IconName {
|
||||||
Tab,
|
Tab,
|
||||||
Terminal,
|
Terminal,
|
||||||
TerminalAlt,
|
TerminalAlt,
|
||||||
|
TerminalGhost,
|
||||||
TextSnippet,
|
TextSnippet,
|
||||||
TextThread,
|
TextThread,
|
||||||
Thread,
|
Thread,
|
||||||
|
|
|
@ -4,7 +4,6 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use settings::get_key_equivalents;
|
|
||||||
use ui::{Button, ButtonStyle};
|
use ui::{Button, ButtonStyle};
|
||||||
use ui::{
|
use ui::{
|
||||||
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
ButtonCommon, Clickable, Context, FluentBuilder, InteractiveElement, Label, LabelCommon,
|
||||||
|
@ -169,7 +168,8 @@ impl Item for KeyContextView {
|
||||||
impl Render for KeyContextView {
|
impl Render for KeyContextView {
|
||||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
let key_equivalents = get_key_equivalents(cx.keyboard_layout().id());
|
|
||||||
|
let key_equivalents = cx.keyboard_mapper().get_key_equivalents();
|
||||||
v_flex()
|
v_flex()
|
||||||
.id("key-context-view")
|
.id("key-context-view")
|
||||||
.overflow_scroll()
|
.overflow_scroll()
|
||||||
|
|
|
@ -1323,7 +1323,7 @@ fn render_copy_code_block_button(
|
||||||
.icon_size(IconSize::Small)
|
.icon_size(IconSize::Small)
|
||||||
.style(ButtonStyle::Filled)
|
.style(ButtonStyle::Filled)
|
||||||
.shape(ui::IconButtonShape::Square)
|
.shape(ui::IconButtonShape::Square)
|
||||||
.tooltip(Tooltip::text("Copy Code"))
|
.tooltip(Tooltip::text("Copy"))
|
||||||
.on_click({
|
.on_click({
|
||||||
let markdown = markdown;
|
let markdown = markdown;
|
||||||
move |_event, _window, cx| {
|
move |_event, _window, cx| {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,7 +3,8 @@ use collections::{BTreeMap, HashMap, IndexMap};
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
Action, ActionBuildError, App, InvalidKeystrokeError, KEYSTROKE_PARSE_EXPECTED_MESSAGE,
|
||||||
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, Keystroke, NoAction, SharedString,
|
KeyBinding, KeyBindingContextPredicate, KeyBindingMetaIndex, KeybindingKeystroke, Keystroke,
|
||||||
|
NoAction, SharedString,
|
||||||
};
|
};
|
||||||
use schemars::{JsonSchema, json_schema};
|
use schemars::{JsonSchema, json_schema};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
@ -211,9 +212,6 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
|
pub fn load(content: &str, cx: &App) -> KeymapFileLoadResult {
|
||||||
let key_equivalents =
|
|
||||||
crate::key_equivalents::get_key_equivalents(cx.keyboard_layout().id());
|
|
||||||
|
|
||||||
if content.is_empty() {
|
if content.is_empty() {
|
||||||
return KeymapFileLoadResult::Success {
|
return KeymapFileLoadResult::Success {
|
||||||
key_bindings: Vec::new(),
|
key_bindings: Vec::new(),
|
||||||
|
@ -255,12 +253,6 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let key_equivalents = if *use_key_equivalents {
|
|
||||||
key_equivalents.as_ref()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut section_errors = String::new();
|
let mut section_errors = String::new();
|
||||||
|
|
||||||
if !unrecognized_fields.is_empty() {
|
if !unrecognized_fields.is_empty() {
|
||||||
|
@ -278,7 +270,7 @@ impl KeymapFile {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
context_predicate.clone(),
|
context_predicate.clone(),
|
||||||
key_equivalents,
|
*use_key_equivalents,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
match result {
|
match result {
|
||||||
|
@ -336,7 +328,7 @@ impl KeymapFile {
|
||||||
keystrokes: &str,
|
keystrokes: &str,
|
||||||
action: &KeymapAction,
|
action: &KeymapAction,
|
||||||
context: Option<Rc<KeyBindingContextPredicate>>,
|
context: Option<Rc<KeyBindingContextPredicate>>,
|
||||||
key_equivalents: Option<&HashMap<char, char>>,
|
use_key_equivalents: bool,
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) -> std::result::Result<KeyBinding, String> {
|
) -> std::result::Result<KeyBinding, String> {
|
||||||
let (build_result, action_input_string) = match &action.0 {
|
let (build_result, action_input_string) = match &action.0 {
|
||||||
|
@ -404,8 +396,9 @@ impl KeymapFile {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
action,
|
action,
|
||||||
context,
|
context,
|
||||||
key_equivalents,
|
use_key_equivalents,
|
||||||
action_input_string.map(SharedString::from),
|
action_input_string.map(SharedString::from),
|
||||||
|
cx.keyboard_mapper().as_ref(),
|
||||||
) {
|
) {
|
||||||
Ok(key_binding) => key_binding,
|
Ok(key_binding) => key_binding,
|
||||||
Err(InvalidKeystrokeError { keystroke }) => {
|
Err(InvalidKeystrokeError { keystroke }) => {
|
||||||
|
@ -607,6 +600,7 @@ impl KeymapFile {
|
||||||
mut operation: KeybindUpdateOperation<'a>,
|
mut operation: KeybindUpdateOperation<'a>,
|
||||||
mut keymap_contents: String,
|
mut keymap_contents: String,
|
||||||
tab_size: usize,
|
tab_size: usize,
|
||||||
|
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
match operation {
|
match operation {
|
||||||
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
|
// if trying to replace a keybinding that is not user-defined, treat it as an add operation
|
||||||
|
@ -646,7 +640,7 @@ impl KeymapFile {
|
||||||
.action_value()
|
.action_value()
|
||||||
.context("Failed to generate target action JSON value")?;
|
.context("Failed to generate target action JSON value")?;
|
||||||
let Some((index, keystrokes_str)) =
|
let Some((index, keystrokes_str)) =
|
||||||
find_binding(&keymap, &target, &target_action_value)
|
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||||
else {
|
else {
|
||||||
anyhow::bail!("Failed to find keybinding to remove");
|
anyhow::bail!("Failed to find keybinding to remove");
|
||||||
};
|
};
|
||||||
|
@ -681,7 +675,7 @@ impl KeymapFile {
|
||||||
.context("Failed to generate source action JSON value")?;
|
.context("Failed to generate source action JSON value")?;
|
||||||
|
|
||||||
if let Some((index, keystrokes_str)) =
|
if let Some((index, keystrokes_str)) =
|
||||||
find_binding(&keymap, &target, &target_action_value)
|
find_binding(&keymap, &target, &target_action_value, keyboard_mapper)
|
||||||
{
|
{
|
||||||
if target.context == source.context {
|
if target.context == source.context {
|
||||||
// if we are only changing the keybinding (common case)
|
// if we are only changing the keybinding (common case)
|
||||||
|
@ -781,7 +775,7 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
let use_key_equivalents = from.and_then(|from| {
|
let use_key_equivalents = from.and_then(|from| {
|
||||||
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
|
let action_value = from.action_value().context("Failed to serialize action value. `use_key_equivalents` on new keybinding may be incorrect.").log_err()?;
|
||||||
let (index, _) = find_binding(&keymap, &from, &action_value)?;
|
let (index, _) = find_binding(&keymap, &from, &action_value, keyboard_mapper)?;
|
||||||
Some(keymap.0[index].use_key_equivalents)
|
Some(keymap.0[index].use_key_equivalents)
|
||||||
}).unwrap_or(false);
|
}).unwrap_or(false);
|
||||||
if use_key_equivalents {
|
if use_key_equivalents {
|
||||||
|
@ -808,6 +802,7 @@ impl KeymapFile {
|
||||||
keymap: &'b KeymapFile,
|
keymap: &'b KeymapFile,
|
||||||
target: &KeybindUpdateTarget<'a>,
|
target: &KeybindUpdateTarget<'a>,
|
||||||
target_action_value: &Value,
|
target_action_value: &Value,
|
||||||
|
keyboard_mapper: &dyn gpui::PlatformKeyboardMapper,
|
||||||
) -> Option<(usize, &'b str)> {
|
) -> Option<(usize, &'b str)> {
|
||||||
let target_context_parsed =
|
let target_context_parsed =
|
||||||
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
KeyBindingContextPredicate::parse(target.context.unwrap_or("")).ok();
|
||||||
|
@ -823,8 +818,11 @@ impl KeymapFile {
|
||||||
for (keystrokes_str, action) in bindings {
|
for (keystrokes_str, action) in bindings {
|
||||||
let Ok(keystrokes) = keystrokes_str
|
let Ok(keystrokes) = keystrokes_str
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
.map(Keystroke::parse)
|
.map(|source| {
|
||||||
.collect::<Result<Vec<_>, _>>()
|
let keystroke = Keystroke::parse(source)?;
|
||||||
|
Ok(KeybindingKeystroke::new(keystroke, false, keyboard_mapper))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, InvalidKeystrokeError>>()
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
@ -832,7 +830,7 @@ impl KeymapFile {
|
||||||
|| !keystrokes
|
|| !keystrokes
|
||||||
.iter()
|
.iter()
|
||||||
.zip(target.keystrokes)
|
.zip(target.keystrokes)
|
||||||
.all(|(a, b)| a.should_match(b))
|
.all(|(a, b)| a.inner.should_match(b))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -847,7 +845,7 @@ impl KeymapFile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum KeybindUpdateOperation<'a> {
|
pub enum KeybindUpdateOperation<'a> {
|
||||||
Replace {
|
Replace {
|
||||||
/// Describes the keybind to create
|
/// Describes the keybind to create
|
||||||
|
@ -916,7 +914,7 @@ impl<'a> KeybindUpdateOperation<'a> {
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct KeybindUpdateTarget<'a> {
|
pub struct KeybindUpdateTarget<'a> {
|
||||||
pub context: Option<&'a str>,
|
pub context: Option<&'a str>,
|
||||||
pub keystrokes: &'a [Keystroke],
|
pub keystrokes: &'a [KeybindingKeystroke],
|
||||||
pub action_name: &'a str,
|
pub action_name: &'a str,
|
||||||
pub action_arguments: Option<&'a str>,
|
pub action_arguments: Option<&'a str>,
|
||||||
}
|
}
|
||||||
|
@ -941,6 +939,9 @@ impl<'a> KeybindUpdateTarget<'a> {
|
||||||
fn keystrokes_unparsed(&self) -> String {
|
fn keystrokes_unparsed(&self) -> String {
|
||||||
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
|
let mut keystrokes = String::with_capacity(self.keystrokes.len() * 8);
|
||||||
for keystroke in self.keystrokes {
|
for keystroke in self.keystrokes {
|
||||||
|
// The reason use `keystroke.unparse()` instead of `keystroke.inner.unparse()`
|
||||||
|
// here is that, we want the user to use `ctrl-shift-4` instead of `ctrl-$`
|
||||||
|
// by default on Windows.
|
||||||
keystrokes.push_str(&keystroke.unparse());
|
keystrokes.push_str(&keystroke.unparse());
|
||||||
keystrokes.push(' ');
|
keystrokes.push(' ');
|
||||||
}
|
}
|
||||||
|
@ -959,7 +960,7 @@ impl<'a> KeybindUpdateTarget<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Debug)]
|
||||||
pub enum KeybindSource {
|
pub enum KeybindSource {
|
||||||
User,
|
User,
|
||||||
Vim,
|
Vim,
|
||||||
|
@ -1020,7 +1021,7 @@ impl From<KeybindSource> for KeyBindingMetaIndex {
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use gpui::Keystroke;
|
use gpui::{DummyKeyboardMapper, KeybindingKeystroke, Keystroke};
|
||||||
use unindent::Unindent;
|
use unindent::Unindent;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
@ -1049,16 +1050,27 @@ mod tests {
|
||||||
operation: KeybindUpdateOperation,
|
operation: KeybindUpdateOperation,
|
||||||
expected: impl ToString,
|
expected: impl ToString,
|
||||||
) {
|
) {
|
||||||
let result = KeymapFile::update_keybinding(operation, input.to_string(), 4)
|
let result = KeymapFile::update_keybinding(
|
||||||
|
operation,
|
||||||
|
input.to_string(),
|
||||||
|
4,
|
||||||
|
&gpui::DummyKeyboardMapper,
|
||||||
|
)
|
||||||
.expect("Update succeeded");
|
.expect("Update succeeded");
|
||||||
pretty_assertions::assert_eq!(expected.to_string(), result);
|
pretty_assertions::assert_eq!(expected.to_string(), result);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
fn parse_keystrokes(keystrokes: &str) -> Vec<Keystroke> {
|
fn parse_keystrokes(keystrokes: &str) -> Vec<KeybindingKeystroke> {
|
||||||
keystrokes
|
keystrokes
|
||||||
.split(' ')
|
.split(' ')
|
||||||
.map(|s| Keystroke::parse(s).expect("Keystrokes valid"))
|
.map(|s| {
|
||||||
|
KeybindingKeystroke::new(
|
||||||
|
Keystroke::parse(s).expect("Keystrokes valid"),
|
||||||
|
false,
|
||||||
|
&DummyKeyboardMapper,
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
mod base_keymap_setting;
|
mod base_keymap_setting;
|
||||||
mod editable_setting_control;
|
mod editable_setting_control;
|
||||||
mod key_equivalents;
|
|
||||||
mod keymap_file;
|
mod keymap_file;
|
||||||
mod settings_file;
|
mod settings_file;
|
||||||
mod settings_json;
|
mod settings_json;
|
||||||
|
@ -14,7 +13,6 @@ use util::asset_str;
|
||||||
|
|
||||||
pub use base_keymap_setting::*;
|
pub use base_keymap_setting::*;
|
||||||
pub use editable_setting_control::*;
|
pub use editable_setting_control::*;
|
||||||
pub use key_equivalents::*;
|
|
||||||
pub use keymap_file::{
|
pub use keymap_file::{
|
||||||
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
|
KeyBindingValidator, KeyBindingValidatorRegistration, KeybindSource, KeybindUpdateOperation,
|
||||||
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
|
KeybindUpdateTarget, KeymapFile, KeymapFileLoadResult,
|
||||||
|
@ -89,7 +87,10 @@ pub fn default_settings() -> Cow<'static, str> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
|
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-macos.json";
|
||||||
|
|
||||||
#[cfg(not(target_os = "macos"))]
|
#[cfg(target_os = "windows")]
|
||||||
|
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-windows.json";
|
||||||
|
|
||||||
|
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
|
||||||
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
|
pub const DEFAULT_KEYMAP_PATH: &str = "keymaps/default-linux.json";
|
||||||
|
|
||||||
pub fn default_keymap() -> Cow<'static, str> {
|
pub fn default_keymap() -> Cow<'static, str> {
|
||||||
|
|
|
@ -14,9 +14,9 @@ use gpui::{
|
||||||
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
Action, AppContext as _, AsyncApp, Axis, ClickEvent, Context, DismissEvent, Entity,
|
||||||
EventEmitter, FocusHandle, Focusable, Global, IsZero,
|
EventEmitter, FocusHandle, Focusable, Global, IsZero,
|
||||||
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
|
KeyBindingContextPredicate::{And, Descendant, Equal, Identifier, Not, NotEqual, Or},
|
||||||
KeyContext, Keystroke, MouseButton, Point, ScrollStrategy, ScrollWheelEvent, Stateful,
|
KeyContext, KeybindingKeystroke, Keystroke, MouseButton, PlatformKeyboardMapper, Point,
|
||||||
StyledText, Subscription, Task, TextStyleRefinement, WeakEntity, actions, anchored, deferred,
|
ScrollStrategy, ScrollWheelEvent, Stateful, StyledText, Subscription, Task,
|
||||||
div,
|
TextStyleRefinement, WeakEntity, actions, anchored, deferred, div,
|
||||||
};
|
};
|
||||||
use language::{Language, LanguageConfig, ToOffset as _};
|
use language::{Language, LanguageConfig, ToOffset as _};
|
||||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||||
|
@ -174,7 +174,7 @@ impl FilterState {
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Hash)]
|
||||||
struct ActionMapping {
|
struct ActionMapping {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: Vec<KeybindingKeystroke>,
|
||||||
context: Option<SharedString>,
|
context: Option<SharedString>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ struct ConflictState {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConflictKeybindMapping = HashMap<
|
type ConflictKeybindMapping = HashMap<
|
||||||
Vec<Keystroke>,
|
Vec<KeybindingKeystroke>,
|
||||||
Vec<(
|
Vec<(
|
||||||
Option<gpui::KeyBindingContextPredicate>,
|
Option<gpui::KeyBindingContextPredicate>,
|
||||||
Vec<ConflictOrigin>,
|
Vec<ConflictOrigin>,
|
||||||
|
@ -414,12 +414,14 @@ impl Focusable for KeymapEditor {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/// Helper function to check if two keystroke sequences match exactly
|
/// Helper function to check if two keystroke sequences match exactly
|
||||||
fn keystrokes_match_exactly(keystrokes1: &[Keystroke], keystrokes2: &[Keystroke]) -> bool {
|
fn keystrokes_match_exactly(
|
||||||
|
keystrokes1: &[KeybindingKeystroke],
|
||||||
|
keystrokes2: &[KeybindingKeystroke],
|
||||||
|
) -> bool {
|
||||||
keystrokes1.len() == keystrokes2.len()
|
keystrokes1.len() == keystrokes2.len()
|
||||||
&& keystrokes1
|
&& keystrokes1.iter().zip(keystrokes2).all(|(k1, k2)| {
|
||||||
.iter()
|
k1.inner.key == k2.inner.key && k1.inner.modifiers == k2.inner.modifiers
|
||||||
.zip(keystrokes2)
|
})
|
||||||
.all(|(k1, k2)| k1.key == k2.key && k1.modifiers == k2.modifiers)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl KeymapEditor {
|
impl KeymapEditor {
|
||||||
|
@ -509,7 +511,7 @@ impl KeymapEditor {
|
||||||
self.filter_editor.read(cx).text(cx)
|
self.filter_editor.read(cx).text(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn current_keystroke_query(&self, cx: &App) -> Vec<Keystroke> {
|
fn current_keystroke_query(&self, cx: &App) -> Vec<KeybindingKeystroke> {
|
||||||
match self.search_mode {
|
match self.search_mode {
|
||||||
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
|
SearchMode::KeyStroke { .. } => self.keystroke_editor.read(cx).keystrokes().to_vec(),
|
||||||
SearchMode::Normal => Default::default(),
|
SearchMode::Normal => Default::default(),
|
||||||
|
@ -530,7 +532,7 @@ impl KeymapEditor {
|
||||||
|
|
||||||
let keystroke_query = keystroke_query
|
let keystroke_query = keystroke_query
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|keystroke| keystroke.unparse())
|
.map(|keystroke| keystroke.inner.unparse())
|
||||||
.collect::<Vec<String>>()
|
.collect::<Vec<String>>()
|
||||||
.join(" ");
|
.join(" ");
|
||||||
|
|
||||||
|
@ -554,7 +556,7 @@ impl KeymapEditor {
|
||||||
async fn update_matches(
|
async fn update_matches(
|
||||||
this: WeakEntity<Self>,
|
this: WeakEntity<Self>,
|
||||||
action_query: String,
|
action_query: String,
|
||||||
keystroke_query: Vec<Keystroke>,
|
keystroke_query: Vec<KeybindingKeystroke>,
|
||||||
cx: &mut AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let action_query = command_palette::normalize_action_query(&action_query);
|
let action_query = command_palette::normalize_action_query(&action_query);
|
||||||
|
@ -603,12 +605,14 @@ impl KeymapEditor {
|
||||||
{
|
{
|
||||||
let query = &keystroke_query[query_cursor];
|
let query = &keystroke_query[query_cursor];
|
||||||
let keystroke = &keystrokes[keystroke_cursor];
|
let keystroke = &keystrokes[keystroke_cursor];
|
||||||
let matches =
|
let matches = query
|
||||||
query.modifiers.is_subset_of(&keystroke.modifiers)
|
.inner
|
||||||
&& ((query.key.is_empty()
|
.modifiers
|
||||||
|| query.key == keystroke.key)
|
.is_subset_of(&keystroke.inner.modifiers)
|
||||||
&& query.key_char.as_ref().is_none_or(
|
&& ((query.inner.key.is_empty()
|
||||||
|q_kc| q_kc == &keystroke.key,
|
|| query.inner.key == keystroke.inner.key)
|
||||||
|
&& query.inner.key_char.as_ref().is_none_or(
|
||||||
|
|q_kc| q_kc == &keystroke.inner.key,
|
||||||
));
|
));
|
||||||
if matches {
|
if matches {
|
||||||
found_count += 1;
|
found_count += 1;
|
||||||
|
@ -678,7 +682,7 @@ impl KeymapEditor {
|
||||||
.map(KeybindSource::from_meta)
|
.map(KeybindSource::from_meta)
|
||||||
.unwrap_or(KeybindSource::Unknown);
|
.unwrap_or(KeybindSource::Unknown);
|
||||||
|
|
||||||
let keystroke_text = ui::text_for_keystrokes(key_binding.keystrokes(), cx);
|
let keystroke_text = ui::text_for_keybinding_keystrokes(key_binding.keystrokes(), cx);
|
||||||
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
|
let ui_key_binding = ui::KeyBinding::new_from_gpui(key_binding.clone(), cx)
|
||||||
.vim_mode(source == KeybindSource::Vim);
|
.vim_mode(source == KeybindSource::Vim);
|
||||||
|
|
||||||
|
@ -1202,7 +1206,10 @@ impl KeymapEditor {
|
||||||
.read(cx)
|
.read(cx)
|
||||||
.get_scrollbar_offset(Axis::Vertical),
|
.get_scrollbar_offset(Axis::Vertical),
|
||||||
));
|
));
|
||||||
cx.spawn(async move |_, _| remove_keybinding(to_remove, &fs, tab_size).await)
|
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||||
|
cx.spawn(async move |_, _| {
|
||||||
|
remove_keybinding(to_remove, &fs, tab_size, keyboard_mapper.as_ref()).await
|
||||||
|
})
|
||||||
.detach_and_notify_err(window, cx);
|
.detach_and_notify_err(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1422,7 +1429,7 @@ impl ProcessedBinding {
|
||||||
.map(|keybind| keybind.get_action_mapping())
|
.map(|keybind| keybind.get_action_mapping())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keystrokes(&self) -> Option<&[Keystroke]> {
|
fn keystrokes(&self) -> Option<&[KeybindingKeystroke]> {
|
||||||
self.ui_key_binding()
|
self.ui_key_binding()
|
||||||
.map(|binding| binding.keystrokes.as_slice())
|
.map(|binding| binding.keystrokes.as_slice())
|
||||||
}
|
}
|
||||||
|
@ -2220,7 +2227,7 @@ impl KeybindingEditorModal {
|
||||||
Ok(action_arguments)
|
Ok(action_arguments)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<Keystroke>> {
|
fn validate_keystrokes(&self, cx: &App) -> anyhow::Result<Vec<KeybindingKeystroke>> {
|
||||||
let new_keystrokes = self
|
let new_keystrokes = self
|
||||||
.keybind_editor
|
.keybind_editor
|
||||||
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
|
.read_with(cx, |editor, _| editor.keystrokes().to_vec());
|
||||||
|
@ -2316,6 +2323,7 @@ impl KeybindingEditorModal {
|
||||||
}).unwrap_or(Ok(()))?;
|
}).unwrap_or(Ok(()))?;
|
||||||
|
|
||||||
let create = self.creating;
|
let create = self.creating;
|
||||||
|
let keyboard_mapper = cx.keyboard_mapper().clone();
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
cx.spawn(async move |this, cx| {
|
||||||
let action_name = existing_keybind.action().name;
|
let action_name = existing_keybind.action().name;
|
||||||
|
@ -2328,6 +2336,7 @@ impl KeybindingEditorModal {
|
||||||
new_action_args.as_deref(),
|
new_action_args.as_deref(),
|
||||||
&fs,
|
&fs,
|
||||||
tab_size,
|
tab_size,
|
||||||
|
keyboard_mapper.as_ref(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
|
@ -2445,11 +2454,21 @@ impl KeybindingEditorModal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_key_char(Keystroke { modifiers, key, .. }: Keystroke) -> Keystroke {
|
fn remove_key_char(
|
||||||
Keystroke {
|
KeybindingKeystroke {
|
||||||
modifiers,
|
inner,
|
||||||
key,
|
display_modifiers,
|
||||||
..Default::default()
|
display_key,
|
||||||
|
}: KeybindingKeystroke,
|
||||||
|
) -> KeybindingKeystroke {
|
||||||
|
KeybindingKeystroke {
|
||||||
|
inner: Keystroke {
|
||||||
|
modifiers: inner.modifiers,
|
||||||
|
key: inner.key,
|
||||||
|
key_char: None,
|
||||||
|
},
|
||||||
|
display_modifiers,
|
||||||
|
display_key,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2992,6 +3011,7 @@ async fn save_keybinding_update(
|
||||||
new_args: Option<&str>,
|
new_args: Option<&str>,
|
||||||
fs: &Arc<dyn Fs>,
|
fs: &Arc<dyn Fs>,
|
||||||
tab_size: usize,
|
tab_size: usize,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
let keymap_contents = settings::KeymapFile::load_keymap_file(fs)
|
||||||
.await
|
.await
|
||||||
|
@ -3034,8 +3054,12 @@ async fn save_keybinding_update(
|
||||||
|
|
||||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||||
|
|
||||||
let updated_keymap_contents =
|
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
operation,
|
||||||
|
keymap_contents,
|
||||||
|
tab_size,
|
||||||
|
keyboard_mapper,
|
||||||
|
)
|
||||||
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
.map_err(|err| anyhow::anyhow!("Could not save updated keybinding: {}", err))?;
|
||||||
fs.write(
|
fs.write(
|
||||||
paths::keymap_file().as_path(),
|
paths::keymap_file().as_path(),
|
||||||
|
@ -3057,6 +3081,7 @@ async fn remove_keybinding(
|
||||||
existing: ProcessedBinding,
|
existing: ProcessedBinding,
|
||||||
fs: &Arc<dyn Fs>,
|
fs: &Arc<dyn Fs>,
|
||||||
tab_size: usize,
|
tab_size: usize,
|
||||||
|
keyboard_mapper: &dyn PlatformKeyboardMapper,
|
||||||
) -> anyhow::Result<()> {
|
) -> anyhow::Result<()> {
|
||||||
let Some(keystrokes) = existing.keystrokes() else {
|
let Some(keystrokes) = existing.keystrokes() else {
|
||||||
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
anyhow::bail!("Cannot remove a keybinding that does not exist");
|
||||||
|
@ -3080,8 +3105,12 @@ async fn remove_keybinding(
|
||||||
};
|
};
|
||||||
|
|
||||||
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
let (new_keybinding, removed_keybinding, source) = operation.generate_telemetry();
|
||||||
let updated_keymap_contents =
|
let updated_keymap_contents = settings::KeymapFile::update_keybinding(
|
||||||
settings::KeymapFile::update_keybinding(operation, keymap_contents, tab_size)
|
operation,
|
||||||
|
keymap_contents,
|
||||||
|
tab_size,
|
||||||
|
keyboard_mapper,
|
||||||
|
)
|
||||||
.context("Failed to update keybinding")?;
|
.context("Failed to update keybinding")?;
|
||||||
fs.write(
|
fs.write(
|
||||||
paths::keymap_file().as_path(),
|
paths::keymap_file().as_path(),
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
Animation, AnimationExt, Context, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
|
||||||
Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
KeybindingKeystroke, Keystroke, Modifiers, ModifiersChangedEvent, Subscription, Task, actions,
|
||||||
};
|
};
|
||||||
use ui::{
|
use ui::{
|
||||||
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
|
ActiveTheme as _, Color, IconButton, IconButtonShape, IconName, IconSize, Label, LabelSize,
|
||||||
|
@ -42,8 +42,8 @@ impl PartialEq for CloseKeystrokeResult {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct KeystrokeInput {
|
pub struct KeystrokeInput {
|
||||||
keystrokes: Vec<Keystroke>,
|
keystrokes: Vec<KeybindingKeystroke>,
|
||||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||||
outer_focus_handle: FocusHandle,
|
outer_focus_handle: FocusHandle,
|
||||||
inner_focus_handle: FocusHandle,
|
inner_focus_handle: FocusHandle,
|
||||||
intercept_subscription: Option<Subscription>,
|
intercept_subscription: Option<Subscription>,
|
||||||
|
@ -70,7 +70,7 @@ impl KeystrokeInput {
|
||||||
const KEYSTROKE_COUNT_MAX: usize = 3;
|
const KEYSTROKE_COUNT_MAX: usize = 3;
|
||||||
|
|
||||||
pub fn new(
|
pub fn new(
|
||||||
placeholder_keystrokes: Option<Vec<Keystroke>>,
|
placeholder_keystrokes: Option<Vec<KeybindingKeystroke>>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
@ -97,7 +97,7 @@ impl KeystrokeInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_keystrokes(&mut self, keystrokes: Vec<Keystroke>, cx: &mut Context<Self>) {
|
pub fn set_keystrokes(&mut self, keystrokes: Vec<KeybindingKeystroke>, cx: &mut Context<Self>) {
|
||||||
self.keystrokes = keystrokes;
|
self.keystrokes = keystrokes;
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
}
|
}
|
||||||
|
@ -106,7 +106,7 @@ impl KeystrokeInput {
|
||||||
self.search = search;
|
self.search = search;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn keystrokes(&self) -> &[Keystroke] {
|
pub fn keystrokes(&self) -> &[KeybindingKeystroke] {
|
||||||
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
if let Some(placeholders) = self.placeholder_keystrokes.as_ref()
|
||||||
&& self.keystrokes.is_empty()
|
&& self.keystrokes.is_empty()
|
||||||
{
|
{
|
||||||
|
@ -116,18 +116,22 @@ impl KeystrokeInput {
|
||||||
&& self
|
&& self
|
||||||
.keystrokes
|
.keystrokes
|
||||||
.last()
|
.last()
|
||||||
.is_some_and(|last| last.key.is_empty())
|
.is_some_and(|last| last.display_key.is_empty())
|
||||||
{
|
{
|
||||||
return &self.keystrokes[..self.keystrokes.len() - 1];
|
return &self.keystrokes[..self.keystrokes.len() - 1];
|
||||||
}
|
}
|
||||||
&self.keystrokes
|
&self.keystrokes
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dummy(modifiers: Modifiers) -> Keystroke {
|
fn dummy(modifiers: Modifiers) -> KeybindingKeystroke {
|
||||||
Keystroke {
|
KeybindingKeystroke {
|
||||||
|
inner: Keystroke {
|
||||||
modifiers,
|
modifiers,
|
||||||
key: "".to_string(),
|
key: "".to_string(),
|
||||||
key_char: None,
|
key_char: None,
|
||||||
|
},
|
||||||
|
display_modifiers: modifiers,
|
||||||
|
display_key: "".to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,7 +258,7 @@ impl KeystrokeInput {
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
|
|
||||||
if let Some(last) = self.keystrokes.last_mut()
|
if let Some(last) = self.keystrokes.last_mut()
|
||||||
&& last.key.is_empty()
|
&& last.display_key.is_empty()
|
||||||
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
&& keystrokes_len <= Self::KEYSTROKE_COUNT_MAX
|
||||||
{
|
{
|
||||||
if !self.search && !event.modifiers.modified() {
|
if !self.search && !event.modifiers.modified() {
|
||||||
|
@ -263,13 +267,15 @@ impl KeystrokeInput {
|
||||||
}
|
}
|
||||||
if self.search {
|
if self.search {
|
||||||
if self.previous_modifiers.modified() {
|
if self.previous_modifiers.modified() {
|
||||||
last.modifiers |= event.modifiers;
|
last.display_modifiers |= event.modifiers;
|
||||||
|
last.inner.modifiers |= event.modifiers;
|
||||||
} else {
|
} else {
|
||||||
self.keystrokes.push(Self::dummy(event.modifiers));
|
self.keystrokes.push(Self::dummy(event.modifiers));
|
||||||
}
|
}
|
||||||
self.previous_modifiers |= event.modifiers;
|
self.previous_modifiers |= event.modifiers;
|
||||||
} else {
|
} else {
|
||||||
last.modifiers = event.modifiers;
|
last.display_modifiers = event.modifiers;
|
||||||
|
last.inner.modifiers = event.modifiers;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
} else if keystrokes_len < Self::KEYSTROKE_COUNT_MAX {
|
||||||
|
@ -297,14 +303,17 @@ impl KeystrokeInput {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut keystroke = keystroke.clone();
|
let mut keystroke =
|
||||||
|
KeybindingKeystroke::new(keystroke.clone(), false, cx.keyboard_mapper().as_ref());
|
||||||
if let Some(last) = self.keystrokes.last()
|
if let Some(last) = self.keystrokes.last()
|
||||||
&& last.key.is_empty()
|
&& last.display_key.is_empty()
|
||||||
&& (!self.search || self.previous_modifiers.modified())
|
&& (!self.search || self.previous_modifiers.modified())
|
||||||
{
|
{
|
||||||
let key = keystroke.key.clone();
|
let display_key = keystroke.display_key.clone();
|
||||||
|
let inner_key = keystroke.inner.key.clone();
|
||||||
keystroke = last.clone();
|
keystroke = last.clone();
|
||||||
keystroke.key = key;
|
keystroke.display_key = display_key;
|
||||||
|
keystroke.inner.key = inner_key;
|
||||||
self.keystrokes.pop();
|
self.keystrokes.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,11 +333,14 @@ impl KeystrokeInput {
|
||||||
self.keystrokes_changed(cx);
|
self.keystrokes_changed(cx);
|
||||||
|
|
||||||
if self.search {
|
if self.search {
|
||||||
self.previous_modifiers = keystroke.modifiers;
|
self.previous_modifiers = keystroke.display_modifiers;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX && keystroke.modifiers.modified() {
|
if self.keystrokes.len() < Self::KEYSTROKE_COUNT_MAX
|
||||||
self.keystrokes.push(Self::dummy(keystroke.modifiers));
|
&& keystroke.display_modifiers.modified()
|
||||||
|
{
|
||||||
|
self.keystrokes
|
||||||
|
.push(Self::dummy(keystroke.display_modifiers));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,7 +376,7 @@ impl KeystrokeInput {
|
||||||
&self.keystrokes
|
&self.keystrokes
|
||||||
};
|
};
|
||||||
keystrokes.iter().map(move |keystroke| {
|
keystrokes.iter().map(move |keystroke| {
|
||||||
h_flex().children(ui::render_keystroke(
|
h_flex().children(ui::render_keybinding_keystroke(
|
||||||
keystroke,
|
keystroke,
|
||||||
Some(Color::Default),
|
Some(Color::Default),
|
||||||
Some(rems(0.875).into()),
|
Some(rems(0.875).into()),
|
||||||
|
@ -809,9 +821,13 @@ mod tests {
|
||||||
/// Verifies that the keystrokes match the expected strings
|
/// Verifies that the keystrokes match the expected strings
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
|
pub fn expect_keystrokes(&mut self, expected: &[&str]) -> &mut Self {
|
||||||
let actual = self
|
let actual: Vec<Keystroke> = self.input.read_with(&self.cx, |input, _| {
|
||||||
.input
|
input
|
||||||
.read_with(&self.cx, |input, _| input.keystrokes.clone());
|
.keystrokes
|
||||||
|
.iter()
|
||||||
|
.map(|keystroke| keystroke.inner.clone())
|
||||||
|
.collect()
|
||||||
|
});
|
||||||
Self::expect_keystrokes_equal(&actual, expected);
|
Self::expect_keystrokes_equal(&actual, expected);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
@ -939,7 +955,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct KeystrokeUpdateTracker {
|
struct KeystrokeUpdateTracker {
|
||||||
initial_keystrokes: Vec<Keystroke>,
|
initial_keystrokes: Vec<KeybindingKeystroke>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
input: Entity<KeystrokeInput>,
|
input: Entity<KeystrokeInput>,
|
||||||
received_keystrokes_updated: bool,
|
received_keystrokes_updated: bool,
|
||||||
|
@ -983,8 +999,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn keystrokes_str(ks: &[Keystroke]) -> String {
|
fn keystrokes_str(ks: &[KeybindingKeystroke]) -> String {
|
||||||
ks.iter().map(|ks| ks.unparse()).join(" ")
|
ks.iter().map(|ks| ks.inner.unparse()).join(" ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -119,7 +119,7 @@ impl Render for OnboardingBanner {
|
||||||
h_flex()
|
h_flex()
|
||||||
.h_full()
|
.h_full()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.child(Icon::new(self.details.icon_name).size(IconSize::Small))
|
.child(Icon::new(self.details.icon_name).size(IconSize::XSmall))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_0p5()
|
.gap_0p5()
|
||||||
|
|
|
@ -275,11 +275,11 @@ impl TitleBar {
|
||||||
|
|
||||||
let banner = cx.new(|cx| {
|
let banner = cx.new(|cx| {
|
||||||
OnboardingBanner::new(
|
OnboardingBanner::new(
|
||||||
"Debugger Onboarding",
|
"ACP Onboarding",
|
||||||
IconName::Debug,
|
IconName::Sparkle,
|
||||||
"The Debugger",
|
"Bring Your Own Agent",
|
||||||
None,
|
Some("Introducing:".into()),
|
||||||
zed_actions::debugger::OpenOnboardingModal.boxed_clone(),
|
zed_actions::agent::OpenAcpOnboardingModal.boxed_clone(),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
|
@ -13,6 +13,9 @@ use crate::prelude::*;
|
||||||
)]
|
)]
|
||||||
#[strum(serialize_all = "snake_case")]
|
#[strum(serialize_all = "snake_case")]
|
||||||
pub enum VectorName {
|
pub enum VectorName {
|
||||||
|
AcpGrid,
|
||||||
|
AcpLogo,
|
||||||
|
AcpLogoSerif,
|
||||||
AiGrid,
|
AiGrid,
|
||||||
DebuggerGrid,
|
DebuggerGrid,
|
||||||
Grid,
|
Grid,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
use crate::PlatformStyle;
|
use crate::PlatformStyle;
|
||||||
use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
|
use crate::{Icon, IconName, IconSize, h_flex, prelude::*};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, AnyElement, App, FocusHandle, Global, IntoElement, Keystroke, Modifiers, Window,
|
Action, AnyElement, App, FocusHandle, Global, IntoElement, KeybindingKeystroke, Keystroke,
|
||||||
relative,
|
Modifiers, Window, relative,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
|
||||||
|
@ -13,7 +13,7 @@ pub struct KeyBinding {
|
||||||
/// More than one keystroke produces a chord.
|
/// More than one keystroke produces a chord.
|
||||||
///
|
///
|
||||||
/// This should always contain at least one keystroke.
|
/// This should always contain at least one keystroke.
|
||||||
pub keystrokes: Vec<Keystroke>,
|
pub keystrokes: Vec<KeybindingKeystroke>,
|
||||||
|
|
||||||
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
/// The [`PlatformStyle`] to use when displaying this keybinding.
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
|
@ -59,7 +59,7 @@ impl KeyBinding {
|
||||||
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
|
cx.try_global::<VimStyle>().is_some_and(|g| g.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(keystrokes: Vec<Keystroke>, cx: &App) -> Self {
|
pub fn new(keystrokes: Vec<KeybindingKeystroke>, cx: &App) -> Self {
|
||||||
Self {
|
Self {
|
||||||
keystrokes,
|
keystrokes,
|
||||||
platform_style: PlatformStyle::platform(),
|
platform_style: PlatformStyle::platform(),
|
||||||
|
@ -99,16 +99,16 @@ impl KeyBinding {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_key(
|
fn render_key(
|
||||||
keystroke: &Keystroke,
|
key: &str,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
size: impl Into<Option<AbsoluteLength>>,
|
size: impl Into<Option<AbsoluteLength>>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
let key_icon = icon_for_key(keystroke, platform_style);
|
let key_icon = icon_for_key(key, platform_style);
|
||||||
match key_icon {
|
match key_icon {
|
||||||
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
Some(icon) => KeyIcon::new(icon, color).size(size).into_any_element(),
|
||||||
None => {
|
None => {
|
||||||
let key = util::capitalize(&keystroke.key);
|
let key = util::capitalize(key);
|
||||||
Key::new(&key, color).size(size).into_any_element()
|
Key::new(&key, color).size(size).into_any_element()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -124,7 +124,7 @@ impl RenderOnce for KeyBinding {
|
||||||
"KEY_BINDING-{}",
|
"KEY_BINDING-{}",
|
||||||
self.keystrokes
|
self.keystrokes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|k| k.key.to_string())
|
.map(|k| k.display_key.to_string())
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" ")
|
.join(" ")
|
||||||
)
|
)
|
||||||
|
@ -137,7 +137,7 @@ impl RenderOnce for KeyBinding {
|
||||||
.py_0p5()
|
.py_0p5()
|
||||||
.rounded_xs()
|
.rounded_xs()
|
||||||
.text_color(cx.theme().colors().text_muted)
|
.text_color(cx.theme().colors().text_muted)
|
||||||
.children(render_keystroke(
|
.children(render_keybinding_keystroke(
|
||||||
keystroke,
|
keystroke,
|
||||||
color,
|
color,
|
||||||
self.size,
|
self.size,
|
||||||
|
@ -148,8 +148,8 @@ impl RenderOnce for KeyBinding {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_keystroke(
|
pub fn render_keybinding_keystroke(
|
||||||
keystroke: &Keystroke,
|
keystroke: &KeybindingKeystroke,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
size: impl Into<Option<AbsoluteLength>>,
|
size: impl Into<Option<AbsoluteLength>>,
|
||||||
platform_style: PlatformStyle,
|
platform_style: PlatformStyle,
|
||||||
|
@ -163,26 +163,39 @@ pub fn render_keystroke(
|
||||||
let size = size.into();
|
let size = size.into();
|
||||||
|
|
||||||
if use_text {
|
if use_text {
|
||||||
let element = Key::new(keystroke_text(keystroke, platform_style, vim_mode), color)
|
let element = Key::new(
|
||||||
|
keystroke_text(
|
||||||
|
&keystroke.display_modifiers,
|
||||||
|
&keystroke.display_key,
|
||||||
|
platform_style,
|
||||||
|
vim_mode,
|
||||||
|
),
|
||||||
|
color,
|
||||||
|
)
|
||||||
.size(size)
|
.size(size)
|
||||||
.into_any_element();
|
.into_any_element();
|
||||||
vec![element]
|
vec![element]
|
||||||
} else {
|
} else {
|
||||||
let mut elements = Vec::new();
|
let mut elements = Vec::new();
|
||||||
elements.extend(render_modifiers(
|
elements.extend(render_modifiers(
|
||||||
&keystroke.modifiers,
|
&keystroke.display_modifiers,
|
||||||
platform_style,
|
platform_style,
|
||||||
color,
|
color,
|
||||||
size,
|
size,
|
||||||
true,
|
true,
|
||||||
));
|
));
|
||||||
elements.push(render_key(keystroke, color, platform_style, size));
|
elements.push(render_key(
|
||||||
|
&keystroke.display_key,
|
||||||
|
color,
|
||||||
|
platform_style,
|
||||||
|
size,
|
||||||
|
));
|
||||||
elements
|
elements
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn icon_for_key(keystroke: &Keystroke, platform_style: PlatformStyle) -> Option<IconName> {
|
fn icon_for_key(key: &str, platform_style: PlatformStyle) -> Option<IconName> {
|
||||||
match keystroke.key.as_str() {
|
match key {
|
||||||
"left" => Some(IconName::ArrowLeft),
|
"left" => Some(IconName::ArrowLeft),
|
||||||
"right" => Some(IconName::ArrowRight),
|
"right" => Some(IconName::ArrowRight),
|
||||||
"up" => Some(IconName::ArrowUp),
|
"up" => Some(IconName::ArrowUp),
|
||||||
|
@ -379,7 +392,7 @@ impl KeyIcon {
|
||||||
/// Returns a textual representation of the key binding for the given [`Action`].
|
/// Returns a textual representation of the key binding for the given [`Action`].
|
||||||
pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
|
pub fn text_for_action(action: &dyn Action, window: &Window, cx: &App) -> Option<String> {
|
||||||
let key_binding = window.highest_precedence_binding_for_action(action)?;
|
let key_binding = window.highest_precedence_binding_for_action(action)?;
|
||||||
Some(text_for_keystrokes(key_binding.keystrokes(), cx))
|
Some(text_for_keybinding_keystrokes(key_binding.keystrokes(), cx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
||||||
|
@ -387,22 +400,50 @@ pub fn text_for_keystrokes(keystrokes: &[Keystroke], cx: &App) -> String {
|
||||||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||||
keystrokes
|
keystrokes
|
||||||
.iter()
|
.iter()
|
||||||
.map(|keystroke| keystroke_text(keystroke, platform_style, vim_enabled))
|
.map(|keystroke| {
|
||||||
|
keystroke_text(
|
||||||
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
|
platform_style,
|
||||||
|
vim_enabled,
|
||||||
|
)
|
||||||
|
})
|
||||||
.join(" ")
|
.join(" ")
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn text_for_keystroke(keystroke: &Keystroke, cx: &App) -> String {
|
pub fn text_for_keybinding_keystrokes(keystrokes: &[KeybindingKeystroke], cx: &App) -> String {
|
||||||
let platform_style = PlatformStyle::platform();
|
let platform_style = PlatformStyle::platform();
|
||||||
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||||
keystroke_text(keystroke, platform_style, vim_enabled)
|
keystrokes
|
||||||
|
.iter()
|
||||||
|
.map(|keystroke| {
|
||||||
|
keystroke_text(
|
||||||
|
&keystroke.display_modifiers,
|
||||||
|
&keystroke.display_key,
|
||||||
|
platform_style,
|
||||||
|
vim_enabled,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.join(" ")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text_for_keystroke(modifiers: &Modifiers, key: &str, cx: &App) -> String {
|
||||||
|
let platform_style = PlatformStyle::platform();
|
||||||
|
let vim_enabled = cx.try_global::<VimStyle>().is_some();
|
||||||
|
keystroke_text(modifiers, key, platform_style, vim_enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a textual representation of the given [`Keystroke`].
|
/// Returns a textual representation of the given [`Keystroke`].
|
||||||
fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode: bool) -> String {
|
fn keystroke_text(
|
||||||
|
modifiers: &Modifiers,
|
||||||
|
key: &str,
|
||||||
|
platform_style: PlatformStyle,
|
||||||
|
vim_mode: bool,
|
||||||
|
) -> String {
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let delimiter = '-';
|
let delimiter = '-';
|
||||||
|
|
||||||
if keystroke.modifiers.function {
|
if modifiers.function {
|
||||||
match vim_mode {
|
match vim_mode {
|
||||||
false => text.push_str("Fn"),
|
false => text.push_str("Fn"),
|
||||||
true => text.push_str("fn"),
|
true => text.push_str("fn"),
|
||||||
|
@ -411,7 +452,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.control {
|
if modifiers.control {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(PlatformStyle::Mac, false) => text.push_str("Control"),
|
(PlatformStyle::Mac, false) => text.push_str("Control"),
|
||||||
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
|
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Ctrl"),
|
||||||
|
@ -421,7 +462,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.platform {
|
if modifiers.platform {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(PlatformStyle::Mac, false) => text.push_str("Command"),
|
(PlatformStyle::Mac, false) => text.push_str("Command"),
|
||||||
(PlatformStyle::Mac, true) => text.push_str("cmd"),
|
(PlatformStyle::Mac, true) => text.push_str("cmd"),
|
||||||
|
@ -434,7 +475,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.alt {
|
if modifiers.alt {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(PlatformStyle::Mac, false) => text.push_str("Option"),
|
(PlatformStyle::Mac, false) => text.push_str("Option"),
|
||||||
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
|
(PlatformStyle::Linux | PlatformStyle::Windows, false) => text.push_str("Alt"),
|
||||||
|
@ -444,7 +485,7 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
text.push(delimiter);
|
text.push(delimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
if keystroke.modifiers.shift {
|
if modifiers.shift {
|
||||||
match (platform_style, vim_mode) {
|
match (platform_style, vim_mode) {
|
||||||
(_, false) => text.push_str("Shift"),
|
(_, false) => text.push_str("Shift"),
|
||||||
(_, true) => text.push_str("shift"),
|
(_, true) => text.push_str("shift"),
|
||||||
|
@ -453,9 +494,9 @@ fn keystroke_text(keystroke: &Keystroke, platform_style: PlatformStyle, vim_mode
|
||||||
}
|
}
|
||||||
|
|
||||||
if vim_mode {
|
if vim_mode {
|
||||||
text.push_str(&keystroke.key)
|
text.push_str(key)
|
||||||
} else {
|
} else {
|
||||||
let key = match keystroke.key.as_str() {
|
let key = match key {
|
||||||
"pageup" => "PageUp",
|
"pageup" => "PageUp",
|
||||||
"pagedown" => "PageDown",
|
"pagedown" => "PageDown",
|
||||||
key => &util::capitalize(key),
|
key => &util::capitalize(key),
|
||||||
|
@ -562,9 +603,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_for_keystroke() {
|
fn test_text_for_keystroke() {
|
||||||
|
let keystroke = Keystroke::parse("cmd-c").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Mac,
|
PlatformStyle::Mac,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -572,7 +615,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Linux,
|
PlatformStyle::Linux,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -580,16 +624,19 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("cmd-c").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Windows,
|
PlatformStyle::Windows,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
"Win-C".to_string()
|
"Win-C".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let keystroke = Keystroke::parse("ctrl-alt-delete").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Mac,
|
PlatformStyle::Mac,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -597,7 +644,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Linux,
|
PlatformStyle::Linux,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -605,16 +653,19 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("ctrl-alt-delete").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Windows,
|
PlatformStyle::Windows,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
"Ctrl-Alt-Delete".to_string()
|
"Ctrl-Alt-Delete".to_string()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let keystroke = Keystroke::parse("shift-pageup").unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Mac,
|
PlatformStyle::Mac,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
@ -622,7 +673,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Linux,
|
PlatformStyle::Linux,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
@ -630,7 +682,8 @@ mod tests {
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
keystroke_text(
|
keystroke_text(
|
||||||
&Keystroke::parse("shift-pageup").unwrap(),
|
&keystroke.modifiers,
|
||||||
|
&keystroke.key,
|
||||||
PlatformStyle::Windows,
|
PlatformStyle::Windows,
|
||||||
false
|
false
|
||||||
),
|
),
|
||||||
|
|
|
@ -203,7 +203,10 @@ impl Vim {
|
||||||
|
|
||||||
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
// hook into the existing to clear out any vim search state on cmd+f or edit -> find.
|
||||||
fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
|
fn search_deploy(&mut self, _: &buffer_search::Deploy, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
// Preserve the current mode when resetting search state
|
||||||
|
let current_mode = self.mode;
|
||||||
self.search = Default::default();
|
self.search = Default::default();
|
||||||
|
self.search.prior_mode = current_mode;
|
||||||
cx.propagate();
|
cx.propagate();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -599,6 +599,13 @@ impl Domain for WorkspaceDb {
|
||||||
ssh_projects ON
|
ssh_projects ON
|
||||||
workspaces.ssh_project_id = ssh_projects.id;
|
workspaces.ssh_project_id = ssh_projects.id;
|
||||||
|
|
||||||
|
DELETE FROM workspaces_2
|
||||||
|
WHERE workspace_id NOT IN (
|
||||||
|
SELECT MAX(workspace_id)
|
||||||
|
FROM workspaces_2
|
||||||
|
GROUP BY ssh_connection_id, paths
|
||||||
|
);
|
||||||
|
|
||||||
DROP TABLE ssh_projects;
|
DROP TABLE ssh_projects;
|
||||||
DROP TABLE workspaces;
|
DROP TABLE workspaces;
|
||||||
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
ALTER TABLE workspaces_2 RENAME TO workspaces;
|
||||||
|
|
|
@ -1308,11 +1308,11 @@ pub fn handle_keymap_file_changes(
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let mut current_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
let mut current_layout_id = cx.keyboard_layout().id().to_string();
|
||||||
cx.on_keyboard_layout_change(move |cx| {
|
cx.on_keyboard_layout_change(move |cx| {
|
||||||
let next_mapping = settings::get_key_equivalents(cx.keyboard_layout().id());
|
let next_layout_id = cx.keyboard_layout().id();
|
||||||
if next_mapping != current_mapping {
|
if next_layout_id != current_layout_id {
|
||||||
current_mapping = next_mapping;
|
current_layout_id = next_layout_id.to_string();
|
||||||
keyboard_layout_tx.unbounded_send(()).ok();
|
keyboard_layout_tx.unbounded_send(()).ok();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -4729,7 +4729,7 @@ mod tests {
|
||||||
// and key strokes contain the given key
|
// and key strokes contain the given key
|
||||||
bindings
|
bindings
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.any(|binding| binding.keystrokes().iter().any(|k| k.key == key)),
|
.any(|binding| binding.keystrokes().iter().any(|k| k.display_key == key)),
|
||||||
"On {} Failed to find {} with key binding {}",
|
"On {} Failed to find {} with key binding {}",
|
||||||
line,
|
line,
|
||||||
action.name(),
|
action.name(),
|
||||||
|
|
|
@ -72,7 +72,10 @@ impl QuickActionBar {
|
||||||
Tooltip::with_meta(
|
Tooltip::with_meta(
|
||||||
tooltip_text,
|
tooltip_text,
|
||||||
Some(open_action_for_tooltip),
|
Some(open_action_for_tooltip),
|
||||||
format!("{} to open in a split", text_for_keystroke(&alt_click, cx)),
|
format!(
|
||||||
|
"{} to open in a split",
|
||||||
|
text_for_keystroke(&alt_click.modifiers, &alt_click.key, cx)
|
||||||
|
),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
|
|
|
@ -284,6 +284,8 @@ pub mod agent {
|
||||||
OpenSettings,
|
OpenSettings,
|
||||||
/// Opens the agent onboarding modal.
|
/// Opens the agent onboarding modal.
|
||||||
OpenOnboardingModal,
|
OpenOnboardingModal,
|
||||||
|
/// Opens the ACP onboarding modal.
|
||||||
|
OpenAcpOnboardingModal,
|
||||||
/// Resets the agent onboarding state.
|
/// Resets the agent onboarding state.
|
||||||
ResetOnboarding,
|
ResetOnboarding,
|
||||||
/// Starts a chat conversation with the agent.
|
/// Starts a chat conversation with the agent.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue