diff --git a/bindings/wysiwyg-ffi/src/ffi_composer_model.rs b/bindings/wysiwyg-ffi/src/ffi_composer_model.rs index 68480d72c..56f145780 100644 --- a/bindings/wysiwyg-ffi/src/ffi_composer_model.rs +++ b/bindings/wysiwyg-ffi/src/ffi_composer_model.rs @@ -270,6 +270,31 @@ impl ComposerModel { )) } + pub fn edit_link_with_text( + self: &Arc, + url: String, + text: String, + attributes: Vec, + ) -> Arc { + let url = Utf16String::from_str(&url); + let text = Utf16String::from_str(&text); + let attrs = attributes + .iter() + .map(|attr| { + ( + Utf16String::from_str(&attr.key), + Utf16String::from_str(&attr.value), + ) + }) + .collect(); + Arc::new(ComposerUpdate::from( + self.inner + .lock() + .unwrap() + .edit_link_with_text(url, text, attrs), + )) + } + /// Creates an at-room mention node and inserts it into the composer at the current selection pub fn insert_at_room_mention(self: &Arc) -> Arc { Arc::new(ComposerUpdate::from( diff --git a/bindings/wysiwyg-ffi/src/ffi_link_actions.rs b/bindings/wysiwyg-ffi/src/ffi_link_actions.rs index 7beba0190..34a9f02d0 100644 --- a/bindings/wysiwyg-ffi/src/ffi_link_actions.rs +++ b/bindings/wysiwyg-ffi/src/ffi_link_actions.rs @@ -4,7 +4,7 @@ use widestring::Utf16String; pub enum LinkAction { CreateWithText, Create, - Edit { url: String }, + Edit { url: String, text: String }, Disabled, } @@ -13,8 +13,9 @@ impl From> for LinkAction { match inner { wysiwyg::LinkAction::CreateWithText => Self::CreateWithText, wysiwyg::LinkAction::Create => Self::Create, - wysiwyg::LinkAction::Edit(url) => Self::Edit { + wysiwyg::LinkAction::Edit(url, text) => Self::Edit { url: url.to_string(), + text: text.to_string(), }, wysiwyg::LinkAction::Disabled => Self::Disabled, } diff --git a/bindings/wysiwyg-wasm/src/lib.rs b/bindings/wysiwyg-wasm/src/lib.rs index e0294a2a5..0e892a64c 100644 --- a/bindings/wysiwyg-wasm/src/lib.rs +++ b/bindings/wysiwyg-wasm/src/lib.rs @@ -316,6 +316,19 @@ impl ComposerModel { )) } + pub fn edit_link_with_text( + &mut self, + url: &str, + text: &str, + attributes: js_sys::Map, + ) -> ComposerUpdate { + ComposerUpdate::from(self.inner.edit_link_with_text( + Utf16String::from_str(url), + Utf16String::from_str(text), + attributes.into_vec(), + )) + } + /// Creates an at-room mention node and inserts it into the composer at the current selection pub fn insert_at_room_mention( &mut self, @@ -831,6 +844,7 @@ pub struct Create; #[wasm_bindgen(getter_with_clone)] pub struct Edit { pub url: String, + pub text: String, } #[derive(Clone)] @@ -860,12 +874,13 @@ impl From> for LinkAction { edit_link: None, disabled: None, }, - wysiwyg::LinkAction::Edit(url) => { + wysiwyg::LinkAction::Edit(url, text) => { let url = url.to_string(); + let text = text.to_string(); Self { create_with_text: None, create: None, - edit_link: Some(Edit { url }), + edit_link: Some(Edit { url, text }), disabled: None, } } diff --git a/crates/wysiwyg/src/composer_model/hyperlinks.rs b/crates/wysiwyg/src/composer_model/hyperlinks.rs index fb8027f97..412f8c618 100644 --- a/crates/wysiwyg/src/composer_model/hyperlinks.rs +++ b/crates/wysiwyg/src/composer_model/hyperlinks.rs @@ -18,6 +18,7 @@ use crate::dom::nodes::dom_node::DomNodeKind; use crate::dom::nodes::dom_node::DomNodeKind::{Link, List}; use crate::dom::nodes::ContainerNodeKind; use crate::dom::nodes::DomNode; +use crate::dom::to_plain_text::ToPlainText; use crate::dom::unicode_string::UnicodeStrExt; use crate::dom::Range; use crate::{ @@ -53,7 +54,10 @@ where LinkAction::Disabled } else { // Otherwise we edit the first link of the selection. - LinkAction::Edit(first_link.get_link_url().unwrap()) + LinkAction::Edit( + first_link.get_link_url().unwrap(), + first_link.to_plain_text(), + ) } } else if s == e || self.is_blank_selection(range) { LinkAction::CreateWithText @@ -121,6 +125,29 @@ where self.set_link_in_range(url, range, attributes) } + pub fn edit_link_with_text( + &mut self, + url: S, + text: S, + attributes: Vec<(S, S)>, + ) -> ComposerUpdate { + self.push_state_to_history(); + let (s, e) = self.safe_selection(); + let range = self.state.dom.find_range(s, e); + let Some(link_loc) = range + .locations + .iter() + .find(|loc| loc.kind == DomNodeKind::Link) else { + panic!("Attempting to edit a link on a range that doesn't contain one") + }; + let start = link_loc.position; + let end = start + link_loc.length; + let new_end = start + text.len(); + self.do_replace_text_in(text, start, end); + let range = self.state.dom.find_range(start, new_end); + self.set_link_in_range(url, range, attributes) + } + fn set_link_in_range( &mut self, mut url: S, diff --git a/crates/wysiwyg/src/link_action.rs b/crates/wysiwyg/src/link_action.rs index 5f361e823..50a243cf9 100644 --- a/crates/wysiwyg/src/link_action.rs +++ b/crates/wysiwyg/src/link_action.rs @@ -24,6 +24,6 @@ pub enum LinkActionUpdate { pub enum LinkAction { CreateWithText, Create, - Edit(S), + Edit(S, S), Disabled, } diff --git a/crates/wysiwyg/src/tests/test_get_link_action.rs b/crates/wysiwyg/src/tests/test_get_link_action.rs index 493f87717..7768e1735 100644 --- a/crates/wysiwyg/src/tests/test_get_link_action.rs +++ b/crates/wysiwyg/src/tests/test_get_link_action.rs @@ -54,7 +54,7 @@ fn get_link_action_from_highlighted_link() { let model = cm("{test}|"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test")) ) } @@ -63,7 +63,7 @@ fn get_link_action_from_cursor_at_the_end_of_a_link() { let model = cm("test|"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test")) ) } @@ -72,7 +72,7 @@ fn get_link_action_from_cursor_inside_a_link() { let model = cm("te|st"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test")) ) } @@ -81,7 +81,7 @@ fn get_link_action_from_cursor_at_the_start_of_a_link() { let model = cm("|test"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test")) ) } @@ -90,7 +90,7 @@ fn get_link_action_from_selection_that_contains_a_link_and_non_links() { let model = cm("{test_bold test}|_link test_bold"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test_link")) ) } @@ -99,7 +99,7 @@ fn get_link_action_from_selection_that_contains_multiple_links() { let model = cm("{test_element test_matrix}|"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test_element")) ) } @@ -108,7 +108,7 @@ fn get_link_action_from_selection_that_contains_multiple_links_partially() { let model = cm("test_{element test}|_matrix"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test_element")) ) } @@ -118,7 +118,7 @@ fn get_link_action_from_selection_that_contains_multiple_links_partially_in_diff let model = cm(" test_{element test}|_matrix"); assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16(" test_element")) ) } @@ -176,7 +176,7 @@ fn get_link_action_on_blank_selection_after_a_link() { // This is the correct behaviour because the end of a link should be considered part of the link itself assert_eq!( model.get_link_action(), - LinkAction::Edit(utf16("https://element.io")) + LinkAction::Edit(utf16("https://element.io"), utf16("test")) ) } @@ -224,7 +224,7 @@ fn get_link_action_on_multiple_link_with_first_immutable() { model.select(Location::from(20), Location::from(20)); assert_eq!( model.get_link_action(), - LinkAction::Edit("https://rust-lang.org".into()), + LinkAction::Edit("https://rust-lang.org".into(), utf16("Rust_mut")), ); } @@ -240,7 +240,7 @@ fn get_link_action_on_multiple_link_with_last_immutable() { model.select(Location::from(0), Location::from(0)); assert_eq!( model.get_link_action(), - LinkAction::Edit("https://rust-lang.org".into()), + LinkAction::Edit("https://rust-lang.org".into(), utf16("Rust_mut")), ); } @@ -281,13 +281,13 @@ fn get_link_action_on_multiple_link_with_first_is_mention() { "#}); assert_eq!( model.get_link_action(), - LinkAction::Edit("https://rust-lang.org".into()), + LinkAction::Edit("https://rust-lang.org".into(), utf16("Rust_mut")), ); // Selecting the link afterwards works model.select(Location::from(10), Location::from(10)); assert_eq!( model.get_link_action(), - LinkAction::Edit("https://rust-lang.org".into()), + LinkAction::Edit("https://rust-lang.org".into(), utf16("Rust_mut")), ); } @@ -300,12 +300,12 @@ fn get_link_action_on_multiple_link_with_last_is_mention() { "#}); assert_eq!( model.get_link_action(), - LinkAction::Edit("https://rust-lang.org".into()), + LinkAction::Edit("https://rust-lang.org".into(), utf16("Rust_mut")), ); // Selecting the mutable link afterwards works model.select(Location::from(0), Location::from(0)); assert_eq!( model.get_link_action(), - LinkAction::Edit("https://rust-lang.org".into()), + LinkAction::Edit("https://rust-lang.org".into(), utf16("Rust_mut")), ); } diff --git a/crates/wysiwyg/src/tests/test_links.rs b/crates/wysiwyg/src/tests/test_links.rs index 5edddb8c9..045ce1321 100644 --- a/crates/wysiwyg/src/tests/test_links.rs +++ b/crates/wysiwyg/src/tests/test_links.rs @@ -943,3 +943,97 @@ fn set_links_in_list_then_add_list_item() { "" ); } + +#[test] +fn edit_entirely_selected_link() { + let mut model = cm("{Mtrix}|"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!(tx(&model), "Matrix|"); +} + +#[test] +fn edit_partially_selected_link() { + let mut model = cm("Mtr{ix}|"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!(tx(&model), "Matrix|"); +} + +#[test] +fn edit_link_from_cursor_position() { + let mut model = cm("Mtrix|"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!(tx(&model), "Matrix|"); +} + +#[test] +fn edit_link_with_multiple_links_edits_first_occurence() { + // Note: this behaviour might change if we allow replacing + // text with the entire extended plain text from the selection. + let mut model = cm("{Mtrix Matrix}|"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!( + tx(&model), + "Matrix| Matrix", + ); +} + +#[test] +fn edit_link_with_multiple_partially_selected_links_edits_first_occurence() { + // Note: this behaviour might change if we allow replacing + // text with the entire extended plain text from the selection. + let mut model = cm("Mt{rix Mat}|rix"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!( + tx(&model), + "Matrix| Matrix", + ); +} + +#[test] +fn edit_entirely_formatted_link_keeps_formatting() { + let mut model = + cm("{Mtrix}|"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!( + tx(&model), + "Matrix|", + ); +} + +#[test] +fn edit_partially_formatted_link_removes_formatting() { + // Note: replacing the text of the link makes the formatting position ambiguous + // it is better to remove it than provide unexpected content. + let mut model = + cm("{Mtrix}|"); + model.edit_link_with_text( + "https://matrix.org".into(), + "Matrix".into(), + vec![], + ); + assert_eq!(tx(&model), "Matrix|"); +}