assistant2: Make context pill names work like editor tabs (#22741)
Context pills for files will now only display the basename of the file. If two files have the same base name, we will also show their parent directories, mimicking the behavior of editor tabs. https://github.com/user-attachments/assets/ee88ee3b-80ff-4115-9ff9-8fe4845a67d8 Note: The double `/` in the file picker is a known separate issue. Release Notes: - N/A --------- Co-authored-by: Danilo <danilo@zed.dev> Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
a827f54022
commit
bcc6d95529
5 changed files with 218 additions and 85 deletions
|
@ -282,13 +282,11 @@ impl ActiveThread {
|
||||||
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
|
.child(div().p_2p5().text_ui(cx).child(markdown.clone()))
|
||||||
.when_some(context, |parent, context| {
|
.when_some(context, |parent, context| {
|
||||||
if !context.is_empty() {
|
if !context.is_empty() {
|
||||||
parent.child(
|
parent.child(h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
|
||||||
h_flex().flex_wrap().gap_1().px_1p5().pb_1p5().children(
|
context.iter().map(|context| {
|
||||||
context
|
ContextPill::new_added(context.clone(), false, None)
|
||||||
.iter()
|
}),
|
||||||
.map(|context| ContextPill::new(context.clone())),
|
))
|
||||||
),
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
parent
|
parent
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,8 @@ impl ContextId {
|
||||||
pub struct Context {
|
pub struct Context {
|
||||||
pub id: ContextId,
|
pub id: ContextId,
|
||||||
pub name: SharedString,
|
pub name: SharedString,
|
||||||
|
pub parent: Option<SharedString>,
|
||||||
|
pub tooltip: Option<SharedString>,
|
||||||
pub kind: ContextKind,
|
pub kind: ContextKind,
|
||||||
pub text: SharedString,
|
pub text: SharedString,
|
||||||
}
|
}
|
||||||
|
@ -40,7 +42,7 @@ pub fn attach_context_to_message(
|
||||||
|
|
||||||
for context in context.into_iter() {
|
for context in context.into_iter() {
|
||||||
match context.kind {
|
match context.kind {
|
||||||
ContextKind::File => {
|
ContextKind::File { .. } => {
|
||||||
file_context.push_str(&context.text);
|
file_context.push_str(&context.text);
|
||||||
file_context.push('\n');
|
file_context.push('\n');
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::{HashMap, HashSet};
|
||||||
use gpui::SharedString;
|
use gpui::SharedString;
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
|
|
||||||
|
@ -60,7 +60,17 @@ impl ContextStore {
|
||||||
let id = self.next_context_id.post_inc();
|
let id = self.next_context_id.post_inc();
|
||||||
self.files.insert(path.to_path_buf(), id);
|
self.files.insert(path.to_path_buf(), id);
|
||||||
|
|
||||||
let name = path.to_string_lossy().into_owned().into();
|
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||||
|
|
||||||
|
let name = match path.file_name() {
|
||||||
|
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||||
|
None => full_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.and_then(|p| p.file_name())
|
||||||
|
.map(|p| p.to_string_lossy().into_owned().into());
|
||||||
|
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
push_fenced_codeblock(path, buffer.text(), &mut text);
|
push_fenced_codeblock(path, buffer.text(), &mut text);
|
||||||
|
@ -68,6 +78,8 @@ impl ContextStore {
|
||||||
self.context.push(Context {
|
self.context.push(Context {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
parent,
|
||||||
|
tooltip: Some(full_path),
|
||||||
kind: ContextKind::File,
|
kind: ContextKind::File,
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
});
|
});
|
||||||
|
@ -77,11 +89,23 @@ impl ContextStore {
|
||||||
let id = self.next_context_id.post_inc();
|
let id = self.next_context_id.post_inc();
|
||||||
self.directories.insert(path.to_path_buf(), id);
|
self.directories.insert(path.to_path_buf(), id);
|
||||||
|
|
||||||
let name = path.to_string_lossy().into_owned().into();
|
let full_path: SharedString = path.to_string_lossy().into_owned().into();
|
||||||
|
|
||||||
|
let name = match path.file_name() {
|
||||||
|
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||||
|
None => full_path.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let parent = path
|
||||||
|
.parent()
|
||||||
|
.and_then(|p| p.file_name())
|
||||||
|
.map(|p| p.to_string_lossy().into_owned().into());
|
||||||
|
|
||||||
self.context.push(Context {
|
self.context.push(Context {
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
|
parent,
|
||||||
|
tooltip: Some(full_path),
|
||||||
kind: ContextKind::Directory,
|
kind: ContextKind::Directory,
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
});
|
});
|
||||||
|
@ -94,6 +118,8 @@ impl ContextStore {
|
||||||
self.context.push(Context {
|
self.context.push(Context {
|
||||||
id: context_id,
|
id: context_id,
|
||||||
name: thread.summary().unwrap_or("New thread".into()),
|
name: thread.summary().unwrap_or("New thread".into()),
|
||||||
|
parent: None,
|
||||||
|
tooltip: None,
|
||||||
kind: ContextKind::Thread,
|
kind: ContextKind::Thread,
|
||||||
text: thread.text().into(),
|
text: thread.text().into(),
|
||||||
});
|
});
|
||||||
|
@ -106,6 +132,8 @@ impl ContextStore {
|
||||||
self.context.push(Context {
|
self.context.push(Context {
|
||||||
id: context_id,
|
id: context_id,
|
||||||
name: url.into(),
|
name: url.into(),
|
||||||
|
parent: None,
|
||||||
|
tooltip: None,
|
||||||
kind: ContextKind::FetchedUrl,
|
kind: ContextKind::FetchedUrl,
|
||||||
text: text.into(),
|
text: text.into(),
|
||||||
});
|
});
|
||||||
|
@ -163,6 +191,19 @@ impl ContextStore {
|
||||||
pub fn included_url(&self, url: &str) -> Option<ContextId> {
|
pub fn included_url(&self, url: &str) -> Option<ContextId> {
|
||||||
self.fetched_urls.get(url).copied()
|
self.fetched_urls.get(url).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn duplicated_names(&self) -> HashSet<SharedString> {
|
||||||
|
let mut seen = HashSet::default();
|
||||||
|
let mut dupes = HashSet::default();
|
||||||
|
|
||||||
|
for context in self.context().iter() {
|
||||||
|
if !seen.insert(&context.name) {
|
||||||
|
dupes.insert(context.name.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dupes
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum IncludedFile {
|
pub enum IncludedFile {
|
||||||
|
|
|
@ -6,6 +6,7 @@ use language::Buffer;
|
||||||
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
use ui::{prelude::*, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip};
|
||||||
use workspace::Workspace;
|
use workspace::Workspace;
|
||||||
|
|
||||||
|
use crate::context::ContextKind;
|
||||||
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
use crate::context_picker::{ConfirmBehavior, ContextPicker};
|
||||||
use crate::context_store::ContextStore;
|
use crate::context_store::ContextStore;
|
||||||
use crate::thread::Thread;
|
use crate::thread::Thread;
|
||||||
|
@ -70,10 +71,13 @@ impl ContextStrip {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = path.to_string_lossy().into_owned().into();
|
let name = match path.file_name() {
|
||||||
|
Some(name) => name.to_string_lossy().into_owned().into(),
|
||||||
|
None => path.to_string_lossy().into_owned().into(),
|
||||||
|
};
|
||||||
|
|
||||||
Some(SuggestedContext::File {
|
Some(SuggestedContext::File {
|
||||||
title,
|
name,
|
||||||
buffer: active_buffer.downgrade(),
|
buffer: active_buffer.downgrade(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -99,7 +103,7 @@ impl ContextStrip {
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(SuggestedContext::Thread {
|
Some(SuggestedContext::Thread {
|
||||||
title: active_thread.summary().unwrap_or("Active Thread".into()),
|
name: active_thread.summary().unwrap_or("New Thread".into()),
|
||||||
thread: weak_active_thread,
|
thread: weak_active_thread,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -114,6 +118,8 @@ impl Render for ContextStrip {
|
||||||
|
|
||||||
let suggested_context = self.suggested_context(cx);
|
let suggested_context = self.suggested_context(cx);
|
||||||
|
|
||||||
|
let dupe_names = context_store.duplicated_names();
|
||||||
|
|
||||||
h_flex()
|
h_flex()
|
||||||
.flex_wrap()
|
.flex_wrap()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
|
@ -165,40 +171,36 @@ impl Render for ContextStrip {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.children(context.iter().map(|context| {
|
.children(context.iter().map(|context| {
|
||||||
ContextPill::new(context.clone()).on_remove({
|
ContextPill::new_added(
|
||||||
let context = context.clone();
|
context.clone(),
|
||||||
let context_store = self.context_store.clone();
|
dupe_names.contains(&context.name),
|
||||||
Rc::new(cx.listener(move |_this, _event, cx| {
|
Some({
|
||||||
context_store.update(cx, |this, _cx| {
|
let context = context.clone();
|
||||||
this.remove_context(&context.id);
|
let context_store = self.context_store.clone();
|
||||||
});
|
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||||
cx.notify();
|
context_store.update(cx, |this, _cx| {
|
||||||
}))
|
this.remove_context(&context.id);
|
||||||
})
|
});
|
||||||
|
cx.notify();
|
||||||
|
}))
|
||||||
|
}),
|
||||||
|
)
|
||||||
}))
|
}))
|
||||||
.when_some(suggested_context, |el, suggested| {
|
.when_some(suggested_context, |el, suggested| {
|
||||||
el.child(
|
el.child(ContextPill::new_suggested(
|
||||||
Button::new("add-suggested-context", suggested.title().clone())
|
suggested.name().clone(),
|
||||||
.on_click({
|
suggested.kind(),
|
||||||
let context_store = self.context_store.clone();
|
{
|
||||||
|
let context_store = self.context_store.clone();
|
||||||
|
Rc::new(cx.listener(move |_this, _event, cx| {
|
||||||
|
context_store.update(cx, |context_store, cx| {
|
||||||
|
suggested.accept(context_store, cx);
|
||||||
|
});
|
||||||
|
|
||||||
cx.listener(move |_this, _event, cx| {
|
cx.notify();
|
||||||
context_store.update(cx, |context_store, cx| {
|
}))
|
||||||
suggested.accept(context_store, cx);
|
},
|
||||||
});
|
))
|
||||||
cx.notify();
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.icon(IconName::Plus)
|
|
||||||
.icon_position(IconPosition::Start)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.label_size(LabelSize::Small)
|
|
||||||
.style(ButtonStyle::Filled)
|
|
||||||
.tooltip(|cx| {
|
|
||||||
Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.when(!context.is_empty(), {
|
.when(!context.is_empty(), {
|
||||||
move |parent| {
|
move |parent| {
|
||||||
|
@ -227,35 +229,42 @@ pub enum SuggestContextKind {
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub enum SuggestedContext {
|
pub enum SuggestedContext {
|
||||||
File {
|
File {
|
||||||
title: SharedString,
|
name: SharedString,
|
||||||
buffer: WeakModel<Buffer>,
|
buffer: WeakModel<Buffer>,
|
||||||
},
|
},
|
||||||
Thread {
|
Thread {
|
||||||
title: SharedString,
|
name: SharedString,
|
||||||
thread: WeakModel<Thread>,
|
thread: WeakModel<Thread>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SuggestedContext {
|
impl SuggestedContext {
|
||||||
pub fn title(&self) -> &SharedString {
|
pub fn name(&self) -> &SharedString {
|
||||||
match self {
|
match self {
|
||||||
Self::File { title, .. } => title,
|
Self::File { name, .. } => name,
|
||||||
Self::Thread { title, .. } => title,
|
Self::Thread { name, .. } => name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
|
pub fn accept(&self, context_store: &mut ContextStore, cx: &mut AppContext) {
|
||||||
match self {
|
match self {
|
||||||
Self::File { buffer, title: _ } => {
|
Self::File { buffer, name: _ } => {
|
||||||
if let Some(buffer) = buffer.upgrade() {
|
if let Some(buffer) = buffer.upgrade() {
|
||||||
context_store.insert_file(buffer.read(cx));
|
context_store.insert_file(buffer.read(cx));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
Self::Thread { thread, title: _ } => {
|
Self::Thread { thread, name: _ } => {
|
||||||
if let Some(thread) = thread.upgrade() {
|
if let Some(thread) = thread.upgrade() {
|
||||||
context_store.insert_thread(thread.read(cx));
|
context_store.insert_thread(thread.read(cx));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> ContextKind {
|
||||||
|
match self {
|
||||||
|
Self::File { .. } => ContextKind::File,
|
||||||
|
Self::Thread { .. } => ContextKind::Thread,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,65 +1,148 @@
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use gpui::ClickEvent;
|
use gpui::ClickEvent;
|
||||||
use ui::{prelude::*, IconButtonShape};
|
use ui::{prelude::*, IconButtonShape, Tooltip};
|
||||||
|
|
||||||
use crate::context::{Context, ContextKind};
|
use crate::context::{Context, ContextKind};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct ContextPill {
|
pub enum ContextPill {
|
||||||
context: Context,
|
Added {
|
||||||
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
context: Context,
|
||||||
|
dupe_name: bool,
|
||||||
|
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||||
|
},
|
||||||
|
Suggested {
|
||||||
|
name: SharedString,
|
||||||
|
kind: ContextKind,
|
||||||
|
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContextPill {
|
impl ContextPill {
|
||||||
pub fn new(context: Context) -> Self {
|
pub fn new_added(
|
||||||
Self {
|
context: Context,
|
||||||
|
dupe_name: bool,
|
||||||
|
on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Added {
|
||||||
context,
|
context,
|
||||||
on_remove: None,
|
dupe_name,
|
||||||
|
on_remove,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_remove(mut self, on_remove: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>) -> Self {
|
pub fn new_suggested(
|
||||||
self.on_remove = Some(on_remove);
|
name: SharedString,
|
||||||
self
|
kind: ContextKind,
|
||||||
|
on_add: Rc<dyn Fn(&ClickEvent, &mut WindowContext)>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Suggested { name, kind, on_add }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> ElementId {
|
||||||
|
match self {
|
||||||
|
Self::Added { context, .. } => {
|
||||||
|
ElementId::NamedInteger("context-pill".into(), context.id.0)
|
||||||
|
}
|
||||||
|
Self::Suggested { .. } => "suggested-context-pill".into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kind(&self) -> &ContextKind {
|
||||||
|
match self {
|
||||||
|
Self::Added { context, .. } => &context.kind,
|
||||||
|
Self::Suggested { kind, .. } => kind,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for ContextPill {
|
impl RenderOnce for ContextPill {
|
||||||
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
let padding_right = if self.on_remove.is_some() {
|
let icon = match &self.kind() {
|
||||||
px(2.)
|
|
||||||
} else {
|
|
||||||
px(4.)
|
|
||||||
};
|
|
||||||
let icon = match self.context.kind {
|
|
||||||
ContextKind::File => IconName::File,
|
ContextKind::File => IconName::File,
|
||||||
ContextKind::Directory => IconName::Folder,
|
ContextKind::Directory => IconName::Folder,
|
||||||
ContextKind::FetchedUrl => IconName::Globe,
|
ContextKind::FetchedUrl => IconName::Globe,
|
||||||
ContextKind::Thread => IconName::MessageCircle,
|
ContextKind::Thread => IconName::MessageCircle,
|
||||||
};
|
};
|
||||||
|
|
||||||
h_flex()
|
let color = cx.theme().colors();
|
||||||
.gap_1()
|
|
||||||
|
let base_pill = h_flex()
|
||||||
|
.id(self.id())
|
||||||
.pl_1()
|
.pl_1()
|
||||||
.pr(padding_right)
|
|
||||||
.pb(px(1.))
|
.pb(px(1.))
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(cx.theme().colors().border.opacity(0.5))
|
|
||||||
.bg(cx.theme().colors().element_background)
|
|
||||||
.rounded_md()
|
.rounded_md()
|
||||||
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted))
|
.gap_1()
|
||||||
.child(Label::new(self.context.name.clone()).size(LabelSize::Small))
|
.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
|
||||||
.when_some(self.on_remove, |parent, on_remove| {
|
|
||||||
parent.child(
|
match &self {
|
||||||
IconButton::new(("remove", self.context.id.0), IconName::Close)
|
ContextPill::Added {
|
||||||
.shape(IconButtonShape::Square)
|
context,
|
||||||
.icon_size(IconSize::XSmall)
|
dupe_name,
|
||||||
.on_click({
|
on_remove,
|
||||||
let on_remove = on_remove.clone();
|
} => base_pill
|
||||||
move |event, cx| on_remove(event, cx)
|
.bg(color.element_background)
|
||||||
}),
|
.border_color(color.border.opacity(0.5))
|
||||||
|
.pr(if on_remove.is_some() { px(2.) } else { px(4.) })
|
||||||
|
.child(Label::new(context.name.clone()).size(LabelSize::Small))
|
||||||
|
.when_some(context.parent.as_ref(), |element, parent_name| {
|
||||||
|
if *dupe_name {
|
||||||
|
element.child(
|
||||||
|
Label::new(parent_name.clone())
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
element
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.when_some(context.tooltip.clone(), |element, tooltip| {
|
||||||
|
element.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||||
|
})
|
||||||
|
.when_some(on_remove.as_ref(), |element, on_remove| {
|
||||||
|
element.child(
|
||||||
|
IconButton::new(("remove", context.id.0), IconName::Close)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.icon_size(IconSize::XSmall)
|
||||||
|
.tooltip(|cx| Tooltip::text("Remove Context", cx))
|
||||||
|
.on_click({
|
||||||
|
let on_remove = on_remove.clone();
|
||||||
|
move |event, cx| on_remove(event, cx)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
ContextPill::Suggested { name, kind, on_add } => base_pill
|
||||||
|
.cursor_pointer()
|
||||||
|
.pr_1()
|
||||||
|
.border_color(color.border_variant.opacity(0.5))
|
||||||
|
.hover(|style| style.bg(color.element_hover.opacity(0.5)))
|
||||||
|
.child(
|
||||||
|
Label::new(name.clone())
|
||||||
|
.size(LabelSize::Small)
|
||||||
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
})
|
.child(
|
||||||
|
Label::new(match kind {
|
||||||
|
ContextKind::File => "Open File",
|
||||||
|
ContextKind::Thread | ContextKind::Directory | ContextKind::FetchedUrl => {
|
||||||
|
"Active"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.size(LabelSize::XSmall)
|
||||||
|
.color(Color::Muted),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Icon::new(IconName::Plus)
|
||||||
|
.size(IconSize::XSmall)
|
||||||
|
.into_any_element(),
|
||||||
|
)
|
||||||
|
.tooltip(|cx| Tooltip::with_meta("Suggested Context", None, "Click to add it", cx))
|
||||||
|
.on_click({
|
||||||
|
let on_add = on_add.clone();
|
||||||
|
move |event, cx| on_add(event, cx)
|
||||||
|
}),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue