@@ -12452,13 +12452,14 @@ impl Editor {
return;
}
- let clipboard_text = Cow::Borrowed(text);
+ let clipboard_text = Cow::Borrowed(text.as_str());
self.transact(window, cx, |this, window, cx| {
let had_active_edit_prediction = this.has_active_edit_prediction();
+ let old_selections = this.selections.all::<usize>(cx);
+ let cursor_offset = this.selections.last::<usize>(cx).head();
if let Some(mut clipboard_selections) = clipboard_selections {
- let old_selections = this.selections.all::<usize>(cx);
let all_selections_were_entire_line =
clipboard_selections.iter().all(|s| s.is_entire_line);
let first_selection_indent_column =
@@ -12466,7 +12467,6 @@ impl Editor {
if clipboard_selections.len() != old_selections.len() {
clipboard_selections.drain(..);
}
- let cursor_offset = this.selections.last::<usize>(cx).head();
let mut auto_indent_on_paste = true;
this.buffer.update(cx, |buffer, cx| {
@@ -12489,22 +12489,36 @@ impl Editor {
start_offset = end_offset + 1;
original_indent_column = Some(clipboard_selection.first_line_indent);
} else {
- to_insert = clipboard_text.as_str();
+ to_insert = &*clipboard_text;
entire_line = all_selections_were_entire_line;
original_indent_column = first_selection_indent_column
}
- // If the corresponding selection was empty when this slice of the
- // clipboard text was written, then the entire line containing the
- // selection was copied. If this selection is also currently empty,
- // then paste the line before the current line of the buffer.
- let range = if selection.is_empty() && handle_entire_lines && entire_line {
- let column = selection.start.to_point(&snapshot).column as usize;
- let line_start = selection.start - column;
- line_start..line_start
- } else {
- selection.range()
- };
+ let (range, to_insert) =
+ if selection.is_empty() && handle_entire_lines && entire_line {
+ // If the corresponding selection was empty when this slice of the
+ // clipboard text was written, then the entire line containing the
+ // selection was copied. If this selection is also currently empty,
+ // then paste the line before the current line of the buffer.
+ let column = selection.start.to_point(&snapshot).column as usize;
+ let line_start = selection.start - column;
+ (line_start..line_start, Cow::Borrowed(to_insert))
+ } else {
+ let language = snapshot.language_at(selection.head());
+ let range = selection.range();
+ if let Some(language) = language
+ && language.name() == "Markdown".into()
+ {
+ edit_for_markdown_paste(
+ &snapshot,
+ range,
+ to_insert,
+ url::Url::parse(to_insert).ok(),
+ )
+ } else {
+ (range, Cow::Borrowed(to_insert))
+ }
+ };
edits.push((range, to_insert));
original_indent_columns.push(original_indent_column);
@@ -12527,7 +12541,53 @@ impl Editor {
let selections = this.selections.all::<usize>(cx);
this.change_selections(Default::default(), window, cx, |s| s.select(selections));
} else {
- this.insert(&clipboard_text, window, cx);
+ let url = url::Url::parse(&clipboard_text).ok();
+
+ let auto_indent_mode = if !clipboard_text.is_empty() {
+ Some(AutoindentMode::Block {
+ original_indent_columns: Vec::new(),
+ })
+ } else {
+ None
+ };
+
+ let selection_anchors = this.buffer.update(cx, |buffer, cx| {
+ let snapshot = buffer.snapshot(cx);
+
+ let anchors = old_selections
+ .iter()
+ .map(|s| {
+ let anchor = snapshot.anchor_after(s.head());
+ s.map(|_| anchor)
+ })
+ .collect::<Vec<_>>();
+
+ let mut edits = Vec::new();
+
+ for selection in old_selections.iter() {
+ let language = snapshot.language_at(selection.head());
+ let range = selection.range();
+
+ let (edit_range, edit_text) = if let Some(language) = language
+ && language.name() == "Markdown".into()
+ {
+ edit_for_markdown_paste(&snapshot, range, &clipboard_text, url.clone())
+ } else {
+ (range, clipboard_text.clone())
+ };
+
+ edits.push((edit_range, edit_text));
+ }
+
+ drop(snapshot);
+ buffer.edit(edits, auto_indent_mode, cx);
+
+ anchors
+ });
+
+ this.change_selections(Default::default(), window, cx, |s| {
+ s.select_anchors(selection_anchors);
+ });
}
let trigger_in_words =
@@ -21679,6 +21739,26 @@ impl Editor {
}
}
+fn edit_for_markdown_paste<'a>(
+ buffer: &MultiBufferSnapshot,
+ range: Range<usize>,
+ to_insert: &'a str,
+ url: Option<url::Url>,
+) -> (Range<usize>, Cow<'a, str>) {
+ if url.is_none() {
+ return (range, Cow::Borrowed(to_insert));
+ };
+
+ let old_text = buffer.text_for_range(range.clone()).collect::<String>();
+
+ let new_text = if range.is_empty() || url::Url::parse(&old_text).is_ok() {
+ Cow::Borrowed(to_insert)
+ } else {
+ Cow::Owned(format!("[{old_text}]({to_insert})"))
+ };
+ (range, new_text)
+}
+
fn vim_enabled(cx: &App) -> bool {
vim_mode_setting::VimModeSetting::try_get(cx)
.map(|vim_mode| vim_mode.0)
@@ -25974,6 +25974,217 @@ let result = variable * 2;",
);
}
+#[gpui::test]
+async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let url = "https://zed.dev";
+
+ let markdown_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Markdown".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
+ cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
+
+ cx.update_editor(|editor, window, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
+ editor.paste(&Paste, window, cx);
+ });
+
+ cx.assert_editor_state(&format!(
+ "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
+ ));
+}
+
+#[gpui::test]
+async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let url = "https://zed.dev";
+
+ let markdown_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Markdown".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
+ cx.set_state(&format!(
+ "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.copy(&Copy, window, cx);
+ });
+
+ cx.set_state(&format!(
+ "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
+ ));
+
+ cx.update_editor(|editor, window, cx| {
+ editor.paste(&Paste, window, cx);
+ });
+
+ cx.assert_editor_state(&format!(
+ "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
+ ));
+}
+
+#[gpui::test]
+async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let url = "https://zed.dev";
+
+ let markdown_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Markdown".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
+ cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
+
+ cx.update_editor(|editor, window, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
+ editor.paste(&Paste, window, cx);
+ });
+
+ cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
+}
+
+#[gpui::test]
+async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let text = "Awesome";
+
+ let markdown_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Markdown".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
+ cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
+
+ cx.update_editor(|editor, window, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
+ editor.paste(&Paste, window, cx);
+ });
+
+ cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
+}
+
+#[gpui::test]
+async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
+ cx: &mut gpui::TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let url = "https://zed.dev";
+
+ let markdown_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Rust".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let mut cx = EditorTestContext::new(cx).await;
+ cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
+ cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
+
+ cx.update_editor(|editor, window, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
+ editor.paste(&Paste, window, cx);
+ });
+
+ cx.assert_editor_state(&format!(
+ "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
+ ));
+}
+
+#[gpui::test]
+async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
+ cx: &mut TestAppContext,
+) {
+ init_test(cx, |_| {});
+
+ let url = "https://zed.dev";
+
+ let markdown_language = Arc::new(Language::new(
+ LanguageConfig {
+ name: "Markdown".into(),
+ ..LanguageConfig::default()
+ },
+ None,
+ ));
+
+ let (editor, cx) = cx.add_window_view(|window, cx| {
+ let multi_buffer = MultiBuffer::build_multi(
+ [
+ ("this will embed -> link", vec![Point::row_range(0..1)]),
+ ("this will replace -> link", vec![Point::row_range(0..1)]),
+ ],
+ cx,
+ );
+ let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
+ editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
+ s.select_ranges(vec![
+ Point::new(0, 19)..Point::new(0, 23),
+ Point::new(1, 21)..Point::new(1, 25),
+ ])
+ });
+ let first_buffer_id = multi_buffer
+ .read(cx)
+ .excerpt_buffer_ids()
+ .into_iter()
+ .next()
+ .unwrap();
+ let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
+ first_buffer.update(cx, |buffer, cx| {
+ buffer.set_language(Some(markdown_language.clone()), cx);
+ });
+
+ editor
+ });
+ let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
+
+ cx.update_editor(|editor, window, cx| {
+ cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
+ editor.paste(&Paste, window, cx);
+ });
+
+ cx.assert_editor_state(&format!(
+ "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
+ ));
+}
+
#[track_caller]
fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
editor