Improve clipboard support on Windows (#51807)
- Simplify and improve Windows clipboard handler. - Fixed components that weren't handling multiple formats on the clipboard properly. Closes https://github.com/zed-industries/zed/issues/51278 Release Notes: - windows: Fixed an issue where text copied from Office applications couldn't be pasted into Zed.
This commit is contained in:
1 parent
07f6b5c06a
commit
471bf7ca7e
8 files changed
+406
-469
No files matched your search
@@ -843,6 +843,17 @@ pub(crate) fn paste_images_as_context(
|
||||
cx: &mut App,
|
||||
) -> Option<Task<()>> {
|
||||
let clipboard = cx.read_from_clipboard()?;
|
||||
|
||||
// Only handle paste if the first clipboard entry is an image or file path.
|
||||
// If text comes first, return None so the caller falls through to text paste.
|
||||
// This respects the priority order set by the source application.
|
||||
if matches!(
|
||||
clipboard.entries().first(),
|
||||
Some(ClipboardEntry::String(_)) | None
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(window.spawn(cx, async move |mut cx| {
|
||||
use itertools::Itertools;
|
||||
let (mut images, paths) = clipboard
|
||||
@@ -885,12 +896,9 @@ pub(crate) fn paste_images_as_context(
|
||||
);
|
||||
}
|
||||
|
||||
cx.update(|_window, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.ok();
|
||||
|
||||
insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await;
|
||||
if !images.is_empty() {
|
||||
insert_images_as_context(images, editor, mention_set, workspace, &mut cx).await;
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -640,15 +640,14 @@ impl MessageEditor {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
.and_then(|entry| match entry {
|
||||
let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
text.metadata_json::<Vec<editor::ClipboardSelection>>()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
@@ -774,14 +773,12 @@ impl MessageEditor {
|
||||
// Handle text paste with potential markdown mention links.
|
||||
// This must be checked BEFORE paste_images_as_context because that function
|
||||
// returns a task even when there are no images in the clipboard.
|
||||
if let Some(clipboard_text) = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
.and_then(|entry| match entry {
|
||||
if let Some(clipboard_text) = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
}) {
|
||||
if clipboard_text.contains("[@") {
|
||||
cx.stop_propagation();
|
||||
let selections_before = self.editor.update(cx, |editor, cx| {
|
||||
|
||||
@@ -1761,15 +1761,14 @@ impl TextThreadEditor {
|
||||
let Some(workspace) = self.workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let editor_clipboard_selections = cx
|
||||
.read_from_clipboard()
|
||||
.and_then(|item| item.entries().first().cloned())
|
||||
.and_then(|entry| match entry {
|
||||
let editor_clipboard_selections = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => {
|
||||
text.metadata_json::<Vec<editor::ClipboardSelection>>()
|
||||
}
|
||||
_ => None,
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
// Insert creases for pasted clipboard selections that:
|
||||
// 1. Contain exactly one selection
|
||||
@@ -1801,7 +1800,14 @@ impl TextThreadEditor {
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_insert_creases && let Some(clipboard_item) = cx.read_from_clipboard() {
|
||||
if let Some(ClipboardEntry::String(clipboard_text)) = clipboard_item.entries().first() {
|
||||
let clipboard_text = clipboard_item
|
||||
.entries()
|
||||
.iter()
|
||||
.find_map(|entry| match entry {
|
||||
ClipboardEntry::String(s) => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
if let Some(clipboard_text) = clipboard_text {
|
||||
if let Some(selections) = editor_clipboard_selections {
|
||||
cx.stop_propagation();
|
||||
|
||||
@@ -1872,65 +1878,60 @@ impl TextThreadEditor {
|
||||
|
||||
cx.stop_propagation();
|
||||
|
||||
let mut images = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::Image(image) = entry {
|
||||
Some(image)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let clipboard_item = cx.read_from_clipboard();
|
||||
|
||||
if let Some(paths) = cx.read_from_clipboard() {
|
||||
for path in paths
|
||||
.into_entries()
|
||||
.filter_map(|entry| {
|
||||
if let ClipboardEntry::ExternalPaths(paths) = entry {
|
||||
Some(paths.paths().to_owned())
|
||||
} else {
|
||||
None
|
||||
let mut images: Vec<gpui::Image> = Vec::new();
|
||||
let mut paths: Vec<std::path::PathBuf> = Vec::new();
|
||||
let mut metadata: Option<CopyMetadata> = None;
|
||||
|
||||
if let Some(item) = &clipboard_item {
|
||||
for entry in item.entries() {
|
||||
match entry {
|
||||
ClipboardEntry::Image(image) => images.push(image.clone()),
|
||||
ClipboardEntry::ExternalPaths(external) => {
|
||||
paths.extend(external.paths().iter().cloned());
|
||||
}
|
||||
})
|
||||
.flatten()
|
||||
{
|
||||
let Ok(content) = std::fs::read(path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
ClipboardEntry::String(text) => {
|
||||
if metadata.is_none() {
|
||||
metadata = text.metadata_json::<CopyMetadata>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let metadata = if let Some(item) = cx.read_from_clipboard() {
|
||||
item.entries().first().and_then(|entry| {
|
||||
if let ClipboardEntry::String(text) = entry {
|
||||
text.metadata_json::<CopyMetadata>()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for path in paths {
|
||||
let Ok(content) = std::fs::read(path) else {
|
||||
continue;
|
||||
};
|
||||
let Ok(format) = image::guess_format(&content) else {
|
||||
continue;
|
||||
};
|
||||
images.push(gpui::Image::from_bytes(
|
||||
match format {
|
||||
image::ImageFormat::Png => gpui::ImageFormat::Png,
|
||||
image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
|
||||
image::ImageFormat::WebP => gpui::ImageFormat::Webp,
|
||||
image::ImageFormat::Gif => gpui::ImageFormat::Gif,
|
||||
image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
|
||||
image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
|
||||
image::ImageFormat::Ico => gpui::ImageFormat::Ico,
|
||||
_ => continue,
|
||||
},
|
||||
content,
|
||||
));
|
||||
}
|
||||
|
||||
// Respect entry priority order — if the first entry is text, the source
|
||||
// application considers text the primary content. Discard collected images
|
||||
// so the text-paste branch runs instead.
|
||||
if clipboard_item
|
||||
.as_ref()
|
||||
.and_then(|item| item.entries().first())
|
||||
.is_some_and(|entry| matches!(entry, ClipboardEntry::String(_)))
|
||||
{
|
||||
images.clear();
|
||||
}
|
||||
|
||||
if images.is_empty() {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
|
||||
+17
-19
@@ -14142,13 +14142,12 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let clipboard_text = match cx.read_from_clipboard() {
|
||||
Some(item) => match item.entries().first() {
|
||||
Some(ClipboardEntry::String(text)) => Some(text.text().to_string()),
|
||||
let clipboard_text = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
})
|
||||
});
|
||||
|
||||
let Some(clipboard_text) = clipboard_text else {
|
||||
log::warn!("Clipboard doesn't contain text.");
|
||||
@@ -14167,19 +14166,18 @@ impl Editor {
|
||||
pub fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.hide_mouse_cursor(HideMouseCursorOrigin::TypingAction, cx);
|
||||
if let Some(item) = cx.read_from_clipboard() {
|
||||
let entries = item.entries();
|
||||
|
||||
match entries.first() {
|
||||
// For now, we only support applying metadata if there's one string. In the future, we can incorporate all the selections
|
||||
// of all the pasted entries.
|
||||
Some(ClipboardEntry::String(clipboard_string)) if entries.len() == 1 => self
|
||||
.do_paste(
|
||||
clipboard_string.text(),
|
||||
clipboard_string.metadata_json::<Vec<ClipboardSelection>>(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
let clipboard_string = item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(s) => Some(s),
|
||||
_ => None,
|
||||
});
|
||||
match clipboard_string {
|
||||
Some(clipboard_string) => self.do_paste(
|
||||
clipboard_string.text(),
|
||||
clipboard_string.metadata_json::<Vec<ClipboardSelection>>(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
),
|
||||
_ => self.do_paste(&item.text().unwrap_or_default(), None, true, window, cx),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,24 +8,22 @@ use windows::Win32::{
|
||||
System::{
|
||||
DataExchange::{
|
||||
CloseClipboard, CountClipboardFormats, EmptyClipboard, EnumClipboardFormats,
|
||||
GetClipboardData, GetClipboardFormatNameW, IsClipboardFormatAvailable, OpenClipboard,
|
||||
RegisterClipboardFormatW, SetClipboardData,
|
||||
GetClipboardData, GetClipboardFormatNameW, OpenClipboard, RegisterClipboardFormatW,
|
||||
SetClipboardData,
|
||||
},
|
||||
Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalSize, GlobalUnlock},
|
||||
Ole::{CF_DIB, CF_HDROP, CF_UNICODETEXT},
|
||||
},
|
||||
UI::Shell::{DragQueryFileW, HDROP},
|
||||
};
|
||||
use windows_core::PCWSTR;
|
||||
use windows::core::{Owned, PCWSTR};
|
||||
|
||||
use gpui::{
|
||||
ClipboardEntry, ClipboardItem, ClipboardString, ExternalPaths, Image, ImageFormat, hash,
|
||||
};
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
|
||||
|
||||
// Clipboard formats
|
||||
static CLIPBOARD_HASH_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("GPUI internal text hash")));
|
||||
static CLIPBOARD_METADATA_FORMAT: LazyLock<u32> =
|
||||
@@ -39,90 +37,15 @@ static CLIPBOARD_PNG_FORMAT: LazyLock<u32> =
|
||||
static CLIPBOARD_JPG_FORMAT: LazyLock<u32> =
|
||||
LazyLock::new(|| register_clipboard_format(windows::core::w!("JFIF")));
|
||||
|
||||
// Helper maps and sets
|
||||
static FORMATS_MAP: LazyLock<FxHashMap<u32, ClipboardFormatType>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashMap::default();
|
||||
formats_map.insert(CF_UNICODETEXT.0 as u32, ClipboardFormatType::Text);
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT, ClipboardFormatType::Image);
|
||||
formats_map.insert(CF_DIB.0 as u32, ClipboardFormatType::Image);
|
||||
formats_map.insert(CF_HDROP.0 as u32, ClipboardFormatType::Files);
|
||||
formats_map
|
||||
});
|
||||
static IMAGE_FORMATS_MAP: LazyLock<FxHashMap<u32, ImageFormat>> = LazyLock::new(|| {
|
||||
let mut formats_map = FxHashMap::default();
|
||||
formats_map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
|
||||
formats_map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif);
|
||||
formats_map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg);
|
||||
formats_map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg);
|
||||
formats_map
|
||||
let mut map = FxHashMap::default();
|
||||
map.insert(*CLIPBOARD_PNG_FORMAT, ImageFormat::Png);
|
||||
map.insert(*CLIPBOARD_GIF_FORMAT, ImageFormat::Gif);
|
||||
map.insert(*CLIPBOARD_JPG_FORMAT, ImageFormat::Jpeg);
|
||||
map.insert(*CLIPBOARD_SVG_FORMAT, ImageFormat::Svg);
|
||||
map
|
||||
});
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum ClipboardFormatType {
|
||||
Text,
|
||||
Image,
|
||||
Files,
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_clipboard(item: ClipboardItem) {
|
||||
with_clipboard(|| write_to_clipboard_inner(item));
|
||||
}
|
||||
|
||||
pub(crate) fn read_from_clipboard() -> Option<ClipboardItem> {
|
||||
with_clipboard(|| {
|
||||
with_best_match_format(|item_format| match format_to_type(item_format) {
|
||||
ClipboardFormatType::Text => read_string_from_clipboard(),
|
||||
ClipboardFormatType::Image => read_image_from_clipboard(item_format),
|
||||
ClipboardFormatType::Files => read_files_from_clipboard(),
|
||||
})
|
||||
})
|
||||
.flatten()
|
||||
}
|
||||
|
||||
pub(crate) fn with_file_names<F>(hdrop: HDROP, mut f: F)
|
||||
where
|
||||
F: FnMut(String),
|
||||
{
|
||||
let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) };
|
||||
for file_index in 0..file_count {
|
||||
let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize;
|
||||
let mut buffer = vec![0u16; filename_length + 1];
|
||||
let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) };
|
||||
if ret == 0 {
|
||||
log::error!("unable to read file name of dragged file");
|
||||
continue;
|
||||
}
|
||||
match String::from_utf16(&buffer[0..filename_length]) {
|
||||
Ok(file_name) => f(file_name),
|
||||
Err(e) => {
|
||||
log::error!("dragged file name is not UTF-16: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn with_clipboard<F, T>(f: F) -> Option<T>
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
{
|
||||
match unsafe { OpenClipboard(None) } {
|
||||
Ok(()) => {
|
||||
let result = f();
|
||||
if let Err(e) = unsafe { CloseClipboard() } {
|
||||
log::error!("Failed to close clipboard: {e}",);
|
||||
}
|
||||
Some(result)
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to open clipboard: {e}",);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_clipboard_format(format: PCWSTR) -> u32 {
|
||||
let ret = unsafe { RegisterClipboardFormatW(format) };
|
||||
if ret == 0 {
|
||||
@@ -139,317 +62,327 @@ fn register_clipboard_format(format: PCWSTR) -> u32 {
|
||||
ret
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_to_type(item_format: u32) -> &'static ClipboardFormatType {
|
||||
FORMATS_MAP.get(&item_format).unwrap()
|
||||
fn get_clipboard_data(format: u32) -> Option<LockedGlobal> {
|
||||
let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0);
|
||||
LockedGlobal::lock(global)
|
||||
}
|
||||
|
||||
// Currently, we only write the first item.
|
||||
fn write_to_clipboard_inner(item: ClipboardItem) -> Result<()> {
|
||||
unsafe {
|
||||
EmptyClipboard()?;
|
||||
}
|
||||
match item.entries().first() {
|
||||
Some(entry) => match entry {
|
||||
ClipboardEntry::String(string) => {
|
||||
write_string_to_clipboard(string)?;
|
||||
pub(crate) fn write_to_clipboard(item: ClipboardItem) {
|
||||
let Some(_clip) = ClipboardGuard::open() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let result: Result<()> = (|| {
|
||||
unsafe { EmptyClipboard()? };
|
||||
for entry in item.entries() {
|
||||
match entry {
|
||||
ClipboardEntry::String(string) => write_string(string)?,
|
||||
ClipboardEntry::Image(image) => write_image(image)?,
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
}
|
||||
ClipboardEntry::Image(image) => {
|
||||
write_image_to_clipboard(image)?;
|
||||
}
|
||||
ClipboardEntry::ExternalPaths(_) => {}
|
||||
},
|
||||
None => {
|
||||
// Writing an empty list of entries just clears the clipboard.
|
||||
}
|
||||
Ok(())
|
||||
})();
|
||||
|
||||
if let Err(e) = result {
|
||||
log::error!("Failed to write to clipboard: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read_from_clipboard() -> Option<ClipboardItem> {
|
||||
let _clip = ClipboardGuard::open()?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
let mut have_text = false;
|
||||
let mut have_image = false;
|
||||
let mut have_files = false;
|
||||
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut format = 0;
|
||||
for _ in 0..count {
|
||||
format = unsafe { EnumClipboardFormats(format) };
|
||||
|
||||
if !have_text && format == CF_UNICODETEXT.0 as u32 {
|
||||
if let Some(entry) = read_string() {
|
||||
entries.push(entry);
|
||||
have_text = true;
|
||||
}
|
||||
} else if !have_image && is_image_format(format) {
|
||||
if let Some(entry) = read_image(format) {
|
||||
entries.push(entry);
|
||||
have_image = true;
|
||||
}
|
||||
} else if !have_files && format == CF_HDROP.0 as u32 {
|
||||
if let Some(entry) = read_files() {
|
||||
entries.push(entry);
|
||||
have_files = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if entries.is_empty() {
|
||||
log_unsupported_clipboard_formats();
|
||||
return None;
|
||||
}
|
||||
Some(ClipboardItem { entries })
|
||||
}
|
||||
|
||||
pub(crate) fn with_file_names<F>(hdrop: HDROP, mut f: F)
|
||||
where
|
||||
F: FnMut(String),
|
||||
{
|
||||
let file_count = unsafe { DragQueryFileW(hdrop, DRAGDROP_GET_FILES_COUNT, None) };
|
||||
for file_index in 0..file_count {
|
||||
let filename_length = unsafe { DragQueryFileW(hdrop, file_index, None) } as usize;
|
||||
let mut buffer = vec![0u16; filename_length + 1];
|
||||
let ret = unsafe { DragQueryFileW(hdrop, file_index, Some(buffer.as_mut_slice())) };
|
||||
if ret == 0 {
|
||||
log::error!("unable to read file name of dragged file");
|
||||
continue;
|
||||
}
|
||||
match String::from_utf16(&buffer[0..filename_length]) {
|
||||
Ok(file_name) => f(file_name),
|
||||
Err(e) => log::error!("dragged file name is not UTF-16: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_clipboard_bytes<T>(data: &[T], format: u32) -> Result<()> {
|
||||
unsafe {
|
||||
let global = Owned::new(GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?);
|
||||
let ptr = GlobalLock(*global);
|
||||
anyhow::ensure!(!ptr.is_null(), "GlobalLock returned null");
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), ptr as _, data.len());
|
||||
GlobalUnlock(*global).ok();
|
||||
SetClipboardData(format, Some(HANDLE(global.0)))?;
|
||||
// SetClipboardData succeeded — the system now owns the memory.
|
||||
std::mem::forget(global);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_string_to_clipboard(item: &ClipboardString) -> Result<()> {
|
||||
let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
|
||||
fn get_clipboard_string(format: u32) -> Option<String> {
|
||||
let locked = get_clipboard_data(format)?;
|
||||
let bytes = locked.as_bytes();
|
||||
let words_len = bytes.len() / std::mem::size_of::<u16>();
|
||||
if words_len == 0 {
|
||||
return Some(String::new());
|
||||
}
|
||||
let slice = unsafe { std::slice::from_raw_parts(bytes.as_ptr() as *const u16, words_len) };
|
||||
let actual_len = slice.iter().position(|&c| c == 0).unwrap_or(words_len);
|
||||
Some(String::from_utf16_lossy(&slice[..actual_len]))
|
||||
}
|
||||
|
||||
fn is_image_format(format: u32) -> bool {
|
||||
IMAGE_FORMATS_MAP.contains_key(&format) || format == CF_DIB.0 as u32
|
||||
}
|
||||
|
||||
fn write_string(item: &ClipboardString) -> Result<()> {
|
||||
let wide: Vec<u16> = item.text.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_clipboard_bytes(&wide, CF_UNICODETEXT.0 as u32)?;
|
||||
|
||||
if let Some(metadata) = item.metadata.as_ref() {
|
||||
let hash_result = {
|
||||
let hash = ClipboardString::text_hash(&item.text);
|
||||
hash.to_ne_bytes()
|
||||
};
|
||||
let encode_wide =
|
||||
unsafe { std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4) };
|
||||
set_data_to_clipboard(encode_wide, *CLIPBOARD_HASH_FORMAT)?;
|
||||
let hash_bytes = ClipboardString::text_hash(&item.text).to_ne_bytes();
|
||||
set_clipboard_bytes(&hash_bytes, *CLIPBOARD_HASH_FORMAT)?;
|
||||
|
||||
let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&metadata_wide, *CLIPBOARD_METADATA_FORMAT)?;
|
||||
let wide: Vec<u16> = metadata.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_clipboard_bytes(&wide, *CLIPBOARD_METADATA_FORMAT)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_data_to_clipboard<T>(data: &[T], format: u32) -> Result<()> {
|
||||
unsafe {
|
||||
let global = GlobalAlloc(GMEM_MOVEABLE, std::mem::size_of_val(data))?;
|
||||
let handle = GlobalLock(global);
|
||||
std::ptr::copy_nonoverlapping(data.as_ptr(), handle as _, data.len());
|
||||
let _ = GlobalUnlock(global);
|
||||
SetClipboardData(format, Some(HANDLE(global.0)))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Here writing PNG to the clipboard to better support other apps. For more info, please ref to
|
||||
// the PR.
|
||||
fn write_image_to_clipboard(item: &Image) -> Result<()> {
|
||||
match item.format {
|
||||
ImageFormat::Svg => set_data_to_clipboard(item.bytes(), *CLIPBOARD_SVG_FORMAT)?,
|
||||
ImageFormat::Gif => {
|
||||
set_data_to_clipboard(item.bytes(), *CLIPBOARD_GIF_FORMAT)?;
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Gif)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
ImageFormat::Png => {
|
||||
set_data_to_clipboard(item.bytes(), *CLIPBOARD_PNG_FORMAT)?;
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Png)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
ImageFormat::Jpeg => {
|
||||
set_data_to_clipboard(item.bytes(), *CLIPBOARD_JPG_FORMAT)?;
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), ImageFormat::Jpeg)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
other => {
|
||||
log::warn!(
|
||||
"Clipboard unsupported image format: {:?}, convert to PNG instead.",
|
||||
item.format
|
||||
);
|
||||
let png_bytes = convert_image_to_png_format(item.bytes(), other)?;
|
||||
set_data_to_clipboard(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_image_to_png_format(bytes: &[u8], image_format: ImageFormat) -> Result<Vec<u8>> {
|
||||
let image =
|
||||
image::load_from_memory_with_format(bytes, gpui_image_format_to_image(image_format))?;
|
||||
let mut output_buf = Vec::new();
|
||||
image.write_to(
|
||||
&mut std::io::Cursor::new(&mut output_buf),
|
||||
image::ImageFormat::Png,
|
||||
)?;
|
||||
Ok(output_buf)
|
||||
}
|
||||
|
||||
// Here, we enumerate all formats on the clipboard and find the first one that we can process.
|
||||
// The reason we don't use `GetPriorityClipboardFormat` is that it sometimes returns the
|
||||
// wrong format.
|
||||
// For instance, when copying a JPEG image from Microsoft Word, there may be several formats
|
||||
// on the clipboard: Jpeg, Png, Svg.
|
||||
// If we use `GetPriorityClipboardFormat`, it will return Svg, which is not what we want.
|
||||
fn with_best_match_format<F>(f: F) -> Option<ClipboardItem>
|
||||
where
|
||||
F: Fn(u32) -> Option<ClipboardEntry>,
|
||||
{
|
||||
let mut text = None;
|
||||
let mut image = None;
|
||||
let mut files = None;
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut clipboard_format = 0;
|
||||
for _ in 0..count {
|
||||
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
|
||||
let Some(item_format) = FORMATS_MAP.get(&clipboard_format) else {
|
||||
continue;
|
||||
};
|
||||
let bucket = match item_format {
|
||||
ClipboardFormatType::Text if text.is_none() => &mut text,
|
||||
ClipboardFormatType::Image if image.is_none() => &mut image,
|
||||
ClipboardFormatType::Files if files.is_none() => &mut files,
|
||||
_ => continue,
|
||||
};
|
||||
if let Some(entry) = f(clipboard_format) {
|
||||
*bucket = Some(entry);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(entry) = [image, files, text].into_iter().flatten().next() {
|
||||
return Some(ClipboardItem {
|
||||
entries: vec![entry],
|
||||
});
|
||||
}
|
||||
|
||||
// log the formats that we don't support yet.
|
||||
{
|
||||
clipboard_format = 0;
|
||||
for _ in 0..count {
|
||||
clipboard_format = unsafe { EnumClipboardFormats(clipboard_format) };
|
||||
let mut buffer = [0u16; 64];
|
||||
unsafe { GetClipboardFormatNameW(clipboard_format, &mut buffer) };
|
||||
let format_name = String::from_utf16_lossy(&buffer);
|
||||
log::warn!(
|
||||
"Try to paste with unsupported clipboard format: {}, {}.",
|
||||
clipboard_format,
|
||||
format_name
|
||||
);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn read_string_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let text = with_clipboard_data(CF_UNICODETEXT.0 as u32, |data_ptr, _| {
|
||||
let pcwstr = PCWSTR(data_ptr as *const u16);
|
||||
String::from_utf16_lossy(unsafe { pcwstr.as_wide() })
|
||||
})?;
|
||||
let Some(hash) = read_hash_from_clipboard() else {
|
||||
return Some(ClipboardEntry::String(ClipboardString::new(text)));
|
||||
fn write_image(item: &Image) -> Result<()> {
|
||||
let native_format = match item.format {
|
||||
ImageFormat::Svg => Some(*CLIPBOARD_SVG_FORMAT),
|
||||
ImageFormat::Gif => Some(*CLIPBOARD_GIF_FORMAT),
|
||||
ImageFormat::Png => Some(*CLIPBOARD_PNG_FORMAT),
|
||||
ImageFormat::Jpeg => Some(*CLIPBOARD_JPG_FORMAT),
|
||||
_ => None,
|
||||
};
|
||||
let Some(metadata) = read_metadata_from_clipboard() else {
|
||||
return Some(ClipboardEntry::String(ClipboardString::new(text)));
|
||||
};
|
||||
if hash == ClipboardString::text_hash(&text) {
|
||||
Some(ClipboardEntry::String(ClipboardString {
|
||||
text,
|
||||
metadata: Some(metadata),
|
||||
}))
|
||||
if let Some(format) = native_format {
|
||||
set_clipboard_bytes(item.bytes(), format)?;
|
||||
}
|
||||
|
||||
// Also provide a PNG copy for broad compatibility.
|
||||
// SVG can't be rasterized by the image crate, so skip it.
|
||||
if item.format != ImageFormat::Svg && native_format != Some(*CLIPBOARD_PNG_FORMAT) {
|
||||
if let Some(png_bytes) = convert_to_png(item.bytes(), item.format) {
|
||||
set_clipboard_bytes(&png_bytes, *CLIPBOARD_PNG_FORMAT)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn convert_to_png(bytes: &[u8], format: ImageFormat) -> Option<Vec<u8>> {
|
||||
let img_format = gpui_to_image_format(format)?;
|
||||
let image = image::load_from_memory_with_format(bytes, img_format)
|
||||
.map_err(|e| log::warn!("Failed to decode image for PNG conversion: {e}"))
|
||||
.ok()?;
|
||||
let mut buf = Vec::new();
|
||||
image
|
||||
.write_to(&mut std::io::Cursor::new(&mut buf), image::ImageFormat::Png)
|
||||
.map_err(|e| log::warn!("Failed to encode PNG: {e}"))
|
||||
.ok()?;
|
||||
Some(buf)
|
||||
}
|
||||
|
||||
fn read_string() -> Option<ClipboardEntry> {
|
||||
let text = get_clipboard_string(CF_UNICODETEXT.0 as u32)?;
|
||||
let metadata = read_clipboard_metadata(&text);
|
||||
Some(ClipboardEntry::String(ClipboardString { text, metadata }))
|
||||
}
|
||||
|
||||
fn read_clipboard_metadata(text: &str) -> Option<String> {
|
||||
let locked = get_clipboard_data(*CLIPBOARD_HASH_FORMAT)?;
|
||||
let hash_bytes: [u8; 8] = locked.as_bytes().get(..8)?.try_into().ok()?;
|
||||
let hash = u64::from_ne_bytes(hash_bytes);
|
||||
if hash != ClipboardString::text_hash(text) {
|
||||
return None;
|
||||
}
|
||||
get_clipboard_string(*CLIPBOARD_METADATA_FORMAT)
|
||||
}
|
||||
|
||||
fn read_image(format: u32) -> Option<ClipboardEntry> {
|
||||
let locked = get_clipboard_data(format)?;
|
||||
let (bytes, image_format) = if format == CF_DIB.0 as u32 {
|
||||
(convert_dib_to_bmp(locked.as_bytes())?, ImageFormat::Bmp)
|
||||
} else {
|
||||
Some(ClipboardEntry::String(ClipboardString::new(text)))
|
||||
}
|
||||
let image_format = *IMAGE_FORMATS_MAP.get(&format)?;
|
||||
(locked.as_bytes().to_vec(), image_format)
|
||||
};
|
||||
let id = hash(&bytes);
|
||||
Some(ClipboardEntry::Image(Image {
|
||||
format: image_format,
|
||||
bytes,
|
||||
id,
|
||||
}))
|
||||
}
|
||||
|
||||
fn read_hash_from_clipboard() -> Option<u64> {
|
||||
if unsafe { IsClipboardFormatAvailable(*CLIPBOARD_HASH_FORMAT).is_err() } {
|
||||
return None;
|
||||
}
|
||||
with_clipboard_data(*CLIPBOARD_HASH_FORMAT, |data_ptr, size| {
|
||||
if size < 8 {
|
||||
return None;
|
||||
}
|
||||
let hash_bytes: [u8; 8] = unsafe {
|
||||
std::slice::from_raw_parts(data_ptr.cast::<u8>(), 8)
|
||||
.try_into()
|
||||
.ok()
|
||||
}?;
|
||||
Some(u64::from_ne_bytes(hash_bytes))
|
||||
})?
|
||||
fn read_files() -> Option<ClipboardEntry> {
|
||||
let locked = get_clipboard_data(CF_HDROP.0 as u32)?;
|
||||
let hdrop = HDROP(locked.ptr as *mut _);
|
||||
let mut filenames = Vec::new();
|
||||
with_file_names(hdrop, |name| filenames.push(std::path::PathBuf::from(name)));
|
||||
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
|
||||
filenames.into(),
|
||||
)))
|
||||
}
|
||||
|
||||
fn read_metadata_from_clipboard() -> Option<String> {
|
||||
unsafe { IsClipboardFormatAvailable(*CLIPBOARD_METADATA_FORMAT).ok()? };
|
||||
with_clipboard_data(*CLIPBOARD_METADATA_FORMAT, |data_ptr, _size| {
|
||||
let pcwstr = PCWSTR(data_ptr as *const u16);
|
||||
String::from_utf16_lossy(unsafe { pcwstr.as_wide() })
|
||||
})
|
||||
}
|
||||
|
||||
fn read_image_from_clipboard(format: u32) -> Option<ClipboardEntry> {
|
||||
// Handle CF_DIB format specially - it's raw bitmap data that needs conversion
|
||||
if format == CF_DIB.0 as u32 {
|
||||
return read_image_for_type(format, ImageFormat::Bmp, Some(convert_dib_to_bmp));
|
||||
}
|
||||
let image_format = format_number_to_image_format(format)?;
|
||||
read_image_for_type::<fn(&[u8]) -> Option<Vec<u8>>>(format, *image_format, None)
|
||||
}
|
||||
|
||||
/// Convert DIB data to BMP file format.
|
||||
/// DIB is essentially BMP without a file header, so we just need to add the 14-byte BITMAPFILEHEADER.
|
||||
fn convert_dib_to_bmp(dib_data: &[u8]) -> Option<Vec<u8>> {
|
||||
if dib_data.len() < 40 {
|
||||
/// DIB is BMP without the 14-byte BITMAPFILEHEADER. Prepend one.
|
||||
fn convert_dib_to_bmp(dib: &[u8]) -> Option<Vec<u8>> {
|
||||
if dib.len() < 40 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let file_size = 14 + dib_data.len() as u32;
|
||||
// Calculate pixel data offset
|
||||
let header_size = u32::from_le_bytes(dib_data[0..4].try_into().ok()?);
|
||||
let bit_count = u16::from_le_bytes(dib_data[14..16].try_into().ok()?);
|
||||
let compression = u32::from_le_bytes(dib_data[16..20].try_into().ok()?);
|
||||
let header_size = u32::from_le_bytes(dib[0..4].try_into().ok()?);
|
||||
let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?);
|
||||
let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?);
|
||||
|
||||
// Calculate color table size
|
||||
let color_table_size = if bit_count <= 8 {
|
||||
let colors_used = u32::from_le_bytes(dib_data[32..36].try_into().ok()?);
|
||||
let num_colors = if colors_used == 0 {
|
||||
let colors_used = u32::from_le_bytes(dib[32..36].try_into().ok()?);
|
||||
(if colors_used == 0 {
|
||||
1u32 << bit_count
|
||||
} else {
|
||||
colors_used
|
||||
};
|
||||
num_colors * 4
|
||||
}) * 4
|
||||
} else if compression == 3 {
|
||||
12 // BI_BITFIELDS
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let pixel_data_offset = 14 + header_size + color_table_size;
|
||||
let pixel_offset = 14 + header_size + color_table_size;
|
||||
let file_size = 14 + dib.len() as u32;
|
||||
|
||||
// Build BITMAPFILEHEADER (14 bytes)
|
||||
let mut bmp_data = Vec::with_capacity(file_size as usize);
|
||||
bmp_data.extend_from_slice(b"BM"); // Signature
|
||||
bmp_data.extend_from_slice(&file_size.to_le_bytes()); // File size
|
||||
bmp_data.extend_from_slice(&[0u8; 4]); // Reserved
|
||||
bmp_data.extend_from_slice(&pixel_data_offset.to_le_bytes()); // Pixel data offset
|
||||
bmp_data.extend_from_slice(dib_data); // DIB data
|
||||
|
||||
Some(bmp_data)
|
||||
let mut bmp = Vec::with_capacity(file_size as usize);
|
||||
bmp.extend_from_slice(b"BM");
|
||||
bmp.extend_from_slice(&file_size.to_le_bytes());
|
||||
bmp.extend_from_slice(&[0u8; 4]); // reserved
|
||||
bmp.extend_from_slice(&pixel_offset.to_le_bytes());
|
||||
bmp.extend_from_slice(dib);
|
||||
Some(bmp)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn format_number_to_image_format(format_number: u32) -> Option<&'static ImageFormat> {
|
||||
IMAGE_FORMATS_MAP.get(&format_number)
|
||||
fn log_unsupported_clipboard_formats() {
|
||||
let count = unsafe { CountClipboardFormats() };
|
||||
let mut format = 0;
|
||||
for _ in 0..count {
|
||||
format = unsafe { EnumClipboardFormats(format) };
|
||||
let mut buffer = [0u16; 64];
|
||||
unsafe { GetClipboardFormatNameW(format, &mut buffer) };
|
||||
let format_name = String::from_utf16_lossy(&buffer);
|
||||
log::warn!(
|
||||
"Try to paste with unsupported clipboard format: {}, {}.",
|
||||
format,
|
||||
format_name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn read_image_for_type<F>(
|
||||
format_number: u32,
|
||||
format: ImageFormat,
|
||||
convert: Option<F>,
|
||||
) -> Option<ClipboardEntry>
|
||||
where
|
||||
F: FnOnce(&[u8]) -> Option<Vec<u8>>,
|
||||
{
|
||||
let (bytes, id) = with_clipboard_data(format_number, |data_ptr, size| {
|
||||
let raw_bytes = unsafe { std::slice::from_raw_parts(data_ptr as *const u8, size) };
|
||||
let bytes = match convert {
|
||||
Some(converter) => converter(raw_bytes)?,
|
||||
None => raw_bytes.to_vec(),
|
||||
};
|
||||
let id = hash(&bytes);
|
||||
Some((bytes, id))
|
||||
})??;
|
||||
Some(ClipboardEntry::Image(Image { format, bytes, id }))
|
||||
fn gpui_to_image_format(value: ImageFormat) -> Option<image::ImageFormat> {
|
||||
match value {
|
||||
ImageFormat::Png => Some(image::ImageFormat::Png),
|
||||
ImageFormat::Jpeg => Some(image::ImageFormat::Jpeg),
|
||||
ImageFormat::Webp => Some(image::ImageFormat::WebP),
|
||||
ImageFormat::Gif => Some(image::ImageFormat::Gif),
|
||||
ImageFormat::Bmp => Some(image::ImageFormat::Bmp),
|
||||
ImageFormat::Tiff => Some(image::ImageFormat::Tiff),
|
||||
other => {
|
||||
log::warn!("No image crate equivalent for format: {other:?}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ClipboardGuard;
|
||||
|
||||
impl ClipboardGuard {
|
||||
fn open() -> Option<Self> {
|
||||
match unsafe { OpenClipboard(None) } {
|
||||
Ok(()) => Some(Self),
|
||||
Err(e) => {
|
||||
log::error!("Failed to open clipboard: {e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn read_files_from_clipboard() -> Option<ClipboardEntry> {
|
||||
let filenames = with_clipboard_data(CF_HDROP.0 as u32, |data_ptr, _size| {
|
||||
let hdrop = HDROP(data_ptr);
|
||||
let mut filenames = Vec::new();
|
||||
with_file_names(hdrop, |file_name| {
|
||||
filenames.push(std::path::PathBuf::from(file_name));
|
||||
});
|
||||
filenames
|
||||
})?;
|
||||
Some(ClipboardEntry::ExternalPaths(ExternalPaths(
|
||||
filenames.into(),
|
||||
)))
|
||||
impl Drop for ClipboardGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Err(e) = unsafe { CloseClipboard() } {
|
||||
log::error!("Failed to close clipboard: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn with_clipboard_data<F, R>(format: u32, f: F) -> Option<R>
|
||||
where
|
||||
F: FnOnce(*mut std::ffi::c_void, usize) -> R,
|
||||
{
|
||||
let global = HGLOBAL(unsafe { GetClipboardData(format).ok() }?.0);
|
||||
let size = unsafe { GlobalSize(global) };
|
||||
let data_ptr = unsafe { GlobalLock(global) };
|
||||
let result = f(data_ptr, size);
|
||||
unsafe { GlobalUnlock(global).ok() };
|
||||
Some(result)
|
||||
struct LockedGlobal {
|
||||
global: HGLOBAL,
|
||||
ptr: *const u8,
|
||||
size: usize,
|
||||
}
|
||||
|
||||
fn gpui_image_format_to_image(value: ImageFormat) -> image::ImageFormat {
|
||||
match value {
|
||||
ImageFormat::Png => image::ImageFormat::Png,
|
||||
ImageFormat::Jpeg => image::ImageFormat::Jpeg,
|
||||
ImageFormat::Webp => image::ImageFormat::WebP,
|
||||
ImageFormat::Gif => image::ImageFormat::Gif,
|
||||
// TODO: ImageFormat::Svg
|
||||
ImageFormat::Bmp => image::ImageFormat::Bmp,
|
||||
ImageFormat::Tiff => image::ImageFormat::Tiff,
|
||||
_ => unreachable!(),
|
||||
impl LockedGlobal {
|
||||
fn lock(global: HGLOBAL) -> Option<Self> {
|
||||
let size = unsafe { GlobalSize(global) };
|
||||
let ptr = unsafe { GlobalLock(global) };
|
||||
if ptr.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
global,
|
||||
ptr: ptr as *const u8,
|
||||
size,
|
||||
})
|
||||
}
|
||||
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
unsafe { std::slice::from_raw_parts(self.ptr, self.size) }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LockedGlobal {
|
||||
fn drop(&mut self) {
|
||||
unsafe { GlobalUnlock(self.global).ok() };
|
||||
}
|
||||
}
|
||||
@@ -813,17 +813,16 @@ impl TerminalView {
|
||||
return;
|
||||
};
|
||||
|
||||
if clipboard.entries().iter().any(|entry| match entry {
|
||||
ClipboardEntry::Image(image) => !image.bytes.is_empty(),
|
||||
_ => false,
|
||||
}) {
|
||||
self.forward_ctrl_v(cx);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(text) = clipboard.text() {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(&text));
|
||||
match clipboard.entries().first() {
|
||||
Some(ClipboardEntry::Image(image)) if !image.bytes.is_empty() => {
|
||||
self.forward_ctrl_v(cx);
|
||||
}
|
||||
_ => {
|
||||
if let Some(text) = clipboard.text() {
|
||||
self.terminal
|
||||
.update(cx, |terminal, _cx| terminal.paste(&text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -282,12 +282,12 @@ impl Vim {
|
||||
/// Pastes the clipboard contents, replacing the same number of characters
|
||||
/// as the clipboard's contents.
|
||||
pub fn paste_replace(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let clipboard_text =
|
||||
cx.read_from_clipboard()
|
||||
.and_then(|item| match item.entries().first() {
|
||||
Some(ClipboardEntry::String(text)) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
});
|
||||
let clipboard_text = cx.read_from_clipboard().and_then(|item| {
|
||||
item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(text) => Some(text.text().to_string()),
|
||||
_ => None,
|
||||
})
|
||||
});
|
||||
|
||||
if let Some(text) = clipboard_text {
|
||||
self.push_operator(Operator::Replace, window, cx);
|
||||
|
||||
@@ -191,14 +191,15 @@ impl From<Register> for ClipboardItem {
|
||||
|
||||
impl From<ClipboardItem> for Register {
|
||||
fn from(item: ClipboardItem) -> Self {
|
||||
// For now, we don't store metadata for multiple entries.
|
||||
match item.entries().first() {
|
||||
Some(ClipboardEntry::String(value)) if item.entries().len() == 1 => Register {
|
||||
match item.entries().iter().find_map(|entry| match entry {
|
||||
ClipboardEntry::String(value) => Some(value),
|
||||
_ => None,
|
||||
}) {
|
||||
Some(value) => Register {
|
||||
text: value.text().to_owned().into(),
|
||||
clipboard_selections: value.metadata_json::<Vec<ClipboardSelection>>(),
|
||||
},
|
||||
// For now, registers can't store images. This could change in the future.
|
||||
_ => Register::default(),
|
||||
None => Register::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user