Support emoji shortcodes in chat (#8455)

Completes: https://github.com/zed-industries/zed/issues/7299

Suggestions


https://github.com/zed-industries/zed/assets/53836821/2a81ba89-4634-4d94-8370-6f76ff3e9403

Automatically replacing shortcodes without using the completions (only
enabled when `message_editor` > `auto_replace_emoji_shortcode` is
enabled in the settings):


https://github.com/zed-industries/zed/assets/53836821/10ef2b4b-c67b-4202-b958-332a37dc088e






Release Notes:

- Added autocompletion for emojis in chat when typing emoji shortcodes
([#7299](https://github.com/zed-industries/zed/issues/7299)).
- Added support for automatically replacing emoji shortcodes in chat
(e.g. typing "👋" will be converted to "👋")

---------

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Bennet Bo Fenner
2024-03-06 19:18:29 -07:00
committed by GitHub
co-authored by Conrad Irwin
parent c0edb5bd6c
commit 8be4b4d75d
10 changed files with 338 additions and 30 deletions
+73
View File
@@ -425,6 +425,7 @@ pub struct Editor {
editor_actions: Vec<Box<dyn Fn(&mut ViewContext<Self>)>>,
show_copilot_suggestions: bool,
use_autoclose: bool,
auto_replace_emoji_shortcode: bool,
custom_context_menu: Option<
Box<
dyn 'static
@@ -1539,6 +1540,7 @@ impl Editor {
use_modal_editing: mode == EditorMode::Full,
read_only: false,
use_autoclose: true,
auto_replace_emoji_shortcode: false,
leader_peer_id: None,
remote_id: None,
hover_state: Default::default(),
@@ -1829,6 +1831,10 @@ impl Editor {
self.use_autoclose = autoclose;
}
pub fn set_auto_replace_emoji_shortcode(&mut self, auto_replace: bool) {
self.auto_replace_emoji_shortcode = auto_replace;
}
pub fn set_show_copilot_suggestions(&mut self, show_copilot_suggestions: bool) {
self.show_copilot_suggestions = show_copilot_suggestions;
}
@@ -2505,6 +2511,47 @@ impl Editor {
}
}
if self.auto_replace_emoji_shortcode
&& selection.is_empty()
&& text.as_ref().ends_with(':')
{
if let Some(possible_emoji_short_code) =
Self::find_possible_emoji_shortcode_at_position(&snapshot, selection.start)
{
if !possible_emoji_short_code.is_empty() {
if let Some(emoji) = emojis::get_by_shortcode(&possible_emoji_short_code) {
let emoji_shortcode_start = Point::new(
selection.start.row,
selection.start.column - possible_emoji_short_code.len() as u32 - 1,
);
// Remove shortcode from buffer
edits.push((
emoji_shortcode_start..selection.start,
"".to_string().into(),
));
new_selections.push((
Selection {
id: selection.id,
start: snapshot.anchor_after(emoji_shortcode_start),
end: snapshot.anchor_before(selection.start),
reversed: selection.reversed,
goal: selection.goal,
},
0,
));
// Insert emoji
let selection_start_anchor = snapshot.anchor_after(selection.start);
new_selections.push((selection.map(|_| selection_start_anchor), 0));
edits.push((selection.start..selection.end, emoji.to_string().into()));
continue;
}
}
}
}
// If not handling any auto-close operation, then just replace the selected
// text with the given input and move the selection to the end of the
// newly inserted text.
@@ -2588,6 +2635,32 @@ impl Editor {
});
}
fn find_possible_emoji_shortcode_at_position(
snapshot: &MultiBufferSnapshot,
position: Point,
) -> Option<String> {
let mut chars = Vec::new();
let mut found_colon = false;
for char in snapshot.reversed_chars_at(position).take(100) {
// Found a possible emoji shortcode in the middle of the buffer
if found_colon && char.is_whitespace() {
chars.reverse();
return Some(chars.iter().collect());
}
if char.is_whitespace() || !char.is_ascii() {
return None;
}
if char == ':' {
found_colon = true;
} else {
chars.push(char);
}
}
// Found a possible emoji shortcode at the beginning of the buffer
chars.reverse();
Some(chars.iter().collect())
}
pub fn newline(&mut self, _: &Newline, cx: &mut ViewContext<Self>) {
self.transact(cx, |this, cx| {
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
+53
View File
@@ -5171,6 +5171,59 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) {
});
}
#[gpui::test]
async fn test_auto_replace_emoji_shortcode(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let language = Arc::new(Language::new(
LanguageConfig::default(),
Some(tree_sitter_rust::language()),
));
let buffer = cx.new_model(|cx| {
Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "")
.with_language(language, cx)
});
let buffer = cx.new_model(|cx| MultiBuffer::singleton(buffer, cx));
let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx));
editor
.condition::<crate::EditorEvent>(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx))
.await;
_ = editor.update(cx, |editor, cx| {
editor.set_auto_replace_emoji_shortcode(true);
editor.handle_input("Hello ", cx);
editor.handle_input(":wave", cx);
assert_eq!(editor.text(cx), "Hello :wave".unindent());
editor.handle_input(":", cx);
assert_eq!(editor.text(cx), "Hello 👋".unindent());
editor.handle_input(" :smile", cx);
assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
editor.handle_input(":", cx);
assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
editor.handle_input(":1:", cx);
assert_eq!(editor.text(cx), "Hello 👋 😄:1:".unindent());
// Ensure shortcode does not get replaced when it is part of a word
editor.handle_input(" Test:wave:", cx);
assert_eq!(editor.text(cx), "Hello 👋 😄:1: Test:wave:".unindent());
editor.set_auto_replace_emoji_shortcode(false);
// Ensure shortcode does not get replaced when auto replace is off
editor.handle_input(" :wave:", cx);
assert_eq!(
editor.text(cx),
"Hello 👋 😄:1: Test:wave: :wave:".unindent()
);
});
}
#[gpui::test]
async fn test_snippets(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});