documents: dropped files and pasted pictures go to the owner; insert puts text at the caret
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
cd16b204f0
commit
d126a7e7f7
9 files changed
+171
-10
No files matched your search
@@ -0,0 +1,62 @@
|
|||||||
|
//! Dropped files and pasted pictures go to the owner.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use gpui::{ClipboardEntry, Context, EventEmitter, Image};
|
||||||
|
|
||||||
|
use super::MarkdownDocument;
|
||||||
|
use crate::documents::MarkdownMode;
|
||||||
|
|
||||||
|
/// What was dropped on or pasted into the document.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub enum Attachment {
|
||||||
|
/// Files dropped, as they lie on disk.
|
||||||
|
Files(Vec<PathBuf>),
|
||||||
|
/// A picture pasted with no text beside it.
|
||||||
|
Picture(Image),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<Attachment> for MarkdownDocument {}
|
||||||
|
|
||||||
|
impl MarkdownDocument {
|
||||||
|
/// A pasted picture goes to the owner; false leaves it.
|
||||||
|
pub(super) fn paste_picture(&mut self, cx: &mut Context<Self>) -> bool {
|
||||||
|
let Some(item) = cx.read_from_clipboard() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let entries = item.entries();
|
||||||
|
if entries
|
||||||
|
.iter()
|
||||||
|
.any(|entry| matches!(entry, ClipboardEntry::String(_)))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(picture) = entries.iter().find_map(|entry| match entry {
|
||||||
|
ClipboardEntry::Image(image) => Some(image.clone()),
|
||||||
|
_ => None,
|
||||||
|
}) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
log::info!("markdown document: a picture pasted");
|
||||||
|
cx.emit(Attachment::Picture(picture));
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `text` at the caret, as typed; false in preview.
|
||||||
|
pub fn insert(&mut self, text: &str, cx: &mut Context<Self>) -> bool {
|
||||||
|
if self.mode == MarkdownMode::Preview {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if let Some(open) = &self.open {
|
||||||
|
open.input.update(cx, |input, cx| input.insert(text, cx));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let caret = self.editor.read(cx).primary().range();
|
||||||
|
let after = caret.start + text.len();
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.edit([(caret, text.to_string())], cx);
|
||||||
|
editor.select(std::iter::once(after..after), cx);
|
||||||
|
});
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
mod attach;
|
||||||
mod edit;
|
mod edit;
|
||||||
mod finding;
|
mod finding;
|
||||||
mod lists;
|
mod lists;
|
||||||
@@ -17,6 +18,7 @@ use gpui::{
|
|||||||
ListState, Subscription, Task, Window,
|
ListState, Subscription, Task, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use attach::Attachment;
|
||||||
pub use parse::DocumentHeading;
|
pub use parse::DocumentHeading;
|
||||||
use parse::{Parsed, alike, parsed};
|
use parse::{Parsed, alike, parsed};
|
||||||
pub use tables::{TableEdit, TableRefusal};
|
pub use tables::{TableEdit, TableRefusal};
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
use std::{cell::RefCell, path::PathBuf, rc::Rc};
|
||||||
|
|
||||||
|
use gpui::{
|
||||||
|
ClipboardItem, Entity, ExternalPaths, FileDropEvent, Image, ImageFormat, Modifiers,
|
||||||
|
TestAppContext, VisualTestContext, point, px,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{MarkdownDocument, MarkdownMode, open, opened, press, text};
|
||||||
|
use crate::documents::Attachment;
|
||||||
|
|
||||||
|
/// Attachments the document hands its owner.
|
||||||
|
fn heard(
|
||||||
|
document: &Entity<MarkdownDocument>,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> Rc<RefCell<Vec<Attachment>>> {
|
||||||
|
let heard = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let kept = heard.clone();
|
||||||
|
cx.update(|_, cx| {
|
||||||
|
cx.subscribe(document, move |_, attached: &Attachment, _| {
|
||||||
|
kept.borrow_mut().push(attached.clone())
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
});
|
||||||
|
heard
|
||||||
|
}
|
||||||
|
|
||||||
|
fn picture() -> Image {
|
||||||
|
Image::from_bytes(ImageFormat::Png, vec![1, 2, 3])
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn dropped_files_go_to_the_owner(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("a\n", MarkdownMode::Visual, cx);
|
||||||
|
let heard = heard(&document, cx);
|
||||||
|
let at = point(px(100.0), px(100.0));
|
||||||
|
let paths = ExternalPaths(vec![PathBuf::from("shot.png")].into());
|
||||||
|
cx.simulate_event(FileDropEvent::Entered {
|
||||||
|
position: at,
|
||||||
|
paths,
|
||||||
|
});
|
||||||
|
cx.simulate_event(FileDropEvent::Submit { position: at });
|
||||||
|
cx.run_until_parked();
|
||||||
|
assert_eq!(
|
||||||
|
*heard.borrow(),
|
||||||
|
[Attachment::Files(vec![PathBuf::from("shot.png")])]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn a_pasted_picture_goes_to_the_owner_and_text_pastes(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("a\n", MarkdownMode::Visual, cx);
|
||||||
|
let heard = heard(&document, cx);
|
||||||
|
press(0, Modifiers::none(), cx);
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_image(&picture()));
|
||||||
|
cx.simulate_keystrokes("secondary-v");
|
||||||
|
assert_eq!(*heard.borrow(), [Attachment::Picture(picture())]);
|
||||||
|
assert_eq!(opened(&document, cx), "a", "the picture typed nothing");
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string("b".into()));
|
||||||
|
cx.simulate_keystrokes("secondary-v");
|
||||||
|
assert_eq!(opened(&document, cx), "ab", "text pastes as text");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
fn text_goes_in_at_the_caret_but_not_in_preview(cx: &mut TestAppContext) {
|
||||||
|
let (document, cx) = open("ab\n", MarkdownMode::Source, cx);
|
||||||
|
document.update(cx, |document, cx| {
|
||||||
|
document.set_caret(0, 1, cx);
|
||||||
|
assert!(document.insert("X", cx));
|
||||||
|
});
|
||||||
|
assert_eq!(text(&document, cx), "aXb\n");
|
||||||
|
document.update_in(cx, |document, window, cx| {
|
||||||
|
document.set_mode(MarkdownMode::Preview, window, cx)
|
||||||
|
});
|
||||||
|
assert!(!document.update(cx, |document, cx| document.insert("Y", cx)));
|
||||||
|
assert_eq!(text(&document, cx), "aXb\n");
|
||||||
|
}
|
||||||
@@ -365,6 +365,7 @@ fn a_press_after_a_write_back_waits_for_the_long_read(cx: &mut TestAppContext) {
|
|||||||
assert_eq!(opened(&document, cx), "B", "once read, B opens whole");
|
assert_eq!(opened(&document, cx), "B", "once read, B opens whole");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod attach;
|
||||||
mod history;
|
mod history;
|
||||||
mod links;
|
mod links;
|
||||||
mod place;
|
mod place;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, App, ClickEvent, Context, FontWeight, InteractiveElement, IntoElement, KeyBinding,
|
AnyElement, App, ClickEvent, Context, ExternalPaths, FontWeight, InteractiveElement,
|
||||||
MouseButton, MouseDownEvent, ParentElement, Render, StatefulInteractiveElement, Styled, Window,
|
IntoElement, KeyBinding, MouseButton, MouseDownEvent, ParentElement, Render,
|
||||||
actions, div, list,
|
StatefulInteractiveElement, Styled, Window, actions, div, list,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::MarkdownDocument;
|
use super::MarkdownDocument;
|
||||||
@@ -12,7 +12,7 @@ use crate::{
|
|||||||
theme::{ActiveTheme, Radius, TextSize},
|
theme::{ActiveTheme, Radius, TextSize},
|
||||||
};
|
};
|
||||||
|
|
||||||
actions!(markdown_document, [Undo, Redo, Indent, Outdent]);
|
actions!(markdown_document, [Undo, Redo, Indent, Outdent, Paste]);
|
||||||
|
|
||||||
const CONTEXT: &str = "MarkdownDocument";
|
const CONTEXT: &str = "MarkdownDocument";
|
||||||
|
|
||||||
@@ -23,6 +23,7 @@ pub(crate) fn bind_keys(cx: &mut App) {
|
|||||||
let mut bindings = vec![
|
let mut bindings = vec![
|
||||||
KeyBinding::new("secondary-z", Undo, context),
|
KeyBinding::new("secondary-z", Undo, context),
|
||||||
KeyBinding::new("secondary-shift-z", Redo, context),
|
KeyBinding::new("secondary-shift-z", Redo, context),
|
||||||
|
KeyBinding::new("secondary-v", Paste, context),
|
||||||
KeyBinding::new("tab", Indent, block),
|
KeyBinding::new("tab", Indent, block),
|
||||||
KeyBinding::new("shift-tab", Outdent, block),
|
KeyBinding::new("shift-tab", Outdent, block),
|
||||||
];
|
];
|
||||||
@@ -144,10 +145,19 @@ impl Render for MarkdownDocument {
|
|||||||
let root = div()
|
let root = div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.bg(colors.surface)
|
.bg(colors.surface)
|
||||||
.font_weight(FontWeight::NORMAL);
|
.font_weight(FontWeight::NORMAL)
|
||||||
|
.on_drop(cx.listener(|_, dropped: &ExternalPaths, _, cx| {
|
||||||
|
log::info!("markdown document: {} files dropped", dropped.paths().len());
|
||||||
|
cx.emit(super::Attachment::Files(dropped.paths().to_vec()));
|
||||||
|
}));
|
||||||
let blocks = list(self.list.clone(), cx.processor(Self::item)).size_full();
|
let blocks = list(self.list.clone(), cx.processor(Self::item)).size_full();
|
||||||
match self.mode {
|
match self.mode {
|
||||||
MarkdownMode::Source => root
|
MarkdownMode::Source => root
|
||||||
|
.capture_action(cx.listener(|document, _: &crate::editor::Paste, _, cx| {
|
||||||
|
if document.paste_picture(cx) {
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
}))
|
||||||
.capture_action(cx.listener(|document, _: &crate::editor::Newline, _, cx| {
|
.capture_action(cx.listener(|document, _: &crate::editor::Newline, _, cx| {
|
||||||
if document.source_newline(cx) {
|
if document.source_newline(cx) {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
@@ -184,6 +194,14 @@ impl Render for MarkdownDocument {
|
|||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
document.history(true, window, cx);
|
document.history(true, window, cx);
|
||||||
}))
|
}))
|
||||||
|
.capture_action(cx.listener(|document, _: &forms::Paste, _, cx| {
|
||||||
|
if document.paste_picture(cx) {
|
||||||
|
cx.stop_propagation();
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.on_action(cx.listener(|document, _: &Paste, _, cx| {
|
||||||
|
document.paste_picture(cx);
|
||||||
|
}))
|
||||||
.capture_action(cx.listener(|document, _: &forms::Enter, _, cx| {
|
.capture_action(cx.listener(|document, _: &forms::Enter, _, cx| {
|
||||||
if document.block_newline(cx) {
|
if document.block_newline(cx) {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ pub use contents::outline;
|
|||||||
pub use counts::{ReadingTime, WordCount};
|
pub use counts::{ReadingTime, WordCount};
|
||||||
pub use diagram::Diagram;
|
pub use diagram::Diagram;
|
||||||
pub use document::{
|
pub use document::{
|
||||||
DocumentEvent, DocumentHeading, LARGEST_READ, MarkdownDocument, TableEdit, TableRefusal,
|
Attachment, DocumentEvent, DocumentHeading, LARGEST_READ, MarkdownDocument, TableEdit,
|
||||||
|
TableRefusal,
|
||||||
};
|
};
|
||||||
pub use epub::{Chapter, EpubReader};
|
pub use epub::{Chapter, EpubReader};
|
||||||
pub use format::{Format, format};
|
pub use format::{Format, format};
|
||||||
|
|||||||
+1
-1
@@ -48,7 +48,7 @@ pub use find::{FindOptions, FindWidget, find_all};
|
|||||||
pub use hints::{CompletionItem, CompletionMenu, Signature, SignatureHelp, completions};
|
pub use hints::{CompletionItem, CompletionMenu, Signature, SignatureHelp, completions};
|
||||||
pub use hover::HoverInfo;
|
pub use hover::HoverInfo;
|
||||||
pub use keybindings::{KeySource, Keybinding, KeybindingsEditor, conflicts};
|
pub use keybindings::{KeySource, Keybinding, KeybindingsEditor, conflicts};
|
||||||
pub(crate) use keys::{Newline, bind_keys};
|
pub(crate) use keys::{Newline, Paste, bind_keys};
|
||||||
pub(crate) use layout::stack;
|
pub(crate) use layout::stack;
|
||||||
pub use preview::ThemePreview;
|
pub use preview::ThemePreview;
|
||||||
pub use problems::{Problem, ProblemsPanel};
|
pub use problems::{Problem, ProblemsPanel};
|
||||||
|
|||||||
+2
-2
@@ -116,8 +116,8 @@ pub use switch::Switch;
|
|||||||
pub(crate) use syntax::{Syntax, syntax_fenced, syntax_of};
|
pub(crate) use syntax::{Syntax, syntax_fenced, syntax_of};
|
||||||
pub use tags::TagInput;
|
pub use tags::TagInput;
|
||||||
pub(crate) use text::{
|
pub(crate) use text::{
|
||||||
Backspace, Copy, Cut, Delete, DeleteWordLeft, Down, Enter, Redo, Submit, Undo, Up, bind_keys,
|
Backspace, Copy, Cut, Delete, DeleteWordLeft, Down, Enter, Paste, Redo, Submit, Undo, Up,
|
||||||
from_utf16, to_utf16,
|
bind_keys, from_utf16, to_utf16,
|
||||||
};
|
};
|
||||||
pub use text::{Highlight, History, InputEvent, TextInput};
|
pub use text::{Highlight, History, InputEvent, TextInput};
|
||||||
pub use transfer::TransferList;
|
pub use transfer::TransferList;
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) use actions::{
|
pub(crate) use actions::{
|
||||||
Backspace, Copy, Cut, Delete, DeleteWordLeft, Down, Enter, Redo, Submit, Undo, Up, bind_keys,
|
Backspace, Copy, Cut, Delete, DeleteWordLeft, Down, Enter, Paste, Redo, Submit, Undo, Up,
|
||||||
|
bind_keys,
|
||||||
};
|
};
|
||||||
pub use edit::History;
|
pub use edit::History;
|
||||||
use edit::Snapshot;
|
use edit::Snapshot;
|
||||||
|
|||||||
Reference in new issue
Block a user