perf: avoid JXA startup for macOS clipboard writes
This commit is contained in:
@@ -20,7 +20,8 @@
|
|||||||
- Space s flips orientation; configurable startup default and shared pane geometry/directional focus.
|
- Space s flips orientation; configurable startup default and shared pane geometry/directional focus.
|
||||||
- [x] add `C` and `D` for deleting line from current cursor position (and entering insert mode: `C`); add `Y` for copying line from current cursor position; add `r` for replacing character at cursor position; add `s` for deleting character at current cursor position and entering insert mode; add `d<count>w` for deleting whole words; add `d<count>b` for deleting backwards (same as `d<count>w` but backwards).
|
- [x] add `C` and `D` for deleting line from current cursor position (and entering insert mode: `C`); add `Y` for copying line from current cursor position; add `r` for replacing character at cursor position; add `s` for deleting character at current cursor position and entering insert mode; add `d<count>w` for deleting whole words; add `d<count>b` for deleting backwards (same as `d<count>w` but backwards).
|
||||||
- [x] diff view: make foreground of etched areas (where no lines exist) the same dimmed color as the normal gutter text
|
- [x] diff view: make foreground of etched areas (where no lines exist) the same dimmed color as the normal gutter text
|
||||||
- [ ] copying is pretty slow; optimize performance to reduce latency.
|
- [x] copying is pretty slow; optimize performance to reduce latency.
|
||||||
|
- [ ] enable margins in bar elements (i.e. for top and bottom bars) and give the lsp symbol (next to language) a default margin of 1 column on either side.
|
||||||
- [ ] pull a pr description and display it in a buffer:
|
- [ ] pull a pr description and display it in a buffer:
|
||||||
- open a selector that allows you to pick a pr from remote
|
- open a selector that allows you to pick a pr from remote
|
||||||
- selector should be searchable
|
- selector should be searchable
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ Diff highlighting lazily owns two instances of the same syntax cache, keyed by t
|
|||||||
|
|
||||||
## Clipboard providers
|
## Clipboard providers
|
||||||
|
|
||||||
macOS uses fixed JXA/AppKit `NSPasteboardTypeString` access through `osascript`, preserving literal RTF/EPS text rather than treating it as rich clipboard data. Linux supports Wayland with `wl-paste`/`wl-copy`, or X11 with `xclip`, on PATH with the corresponding display environment. Other platforms report unsupported access. Commands have a two-second deadline and bounded output; failures terminate/reap their process group.
|
macOS publishes `public.utf8-plain-text` through the native Pasteboard Manager, checking allocation and OS-status failures before reporting success. Publication is synchronous and avoids per-copy interpreter/process startup; native calls do not have a subprocess deadline. Reads retain fixed JXA/AppKit `NSPasteboardTypeString` access through `osascript`. Both paths preserve literal RTF/EPS text rather than treating it as rich clipboard data. Linux supports Wayland with `wl-paste`/`wl-copy`, or X11 with `xclip`, on PATH with the corresponding display environment. Other platforms report unsupported access. Subprocess-backed access has a two-second deadline and bounded output; failures terminate/reap the process group.
|
||||||
|
|
||||||
## Native LSP
|
## Native LSP
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -134,7 +134,7 @@ For other applications, characterwise multi-selection exports join fragments wit
|
|||||||
|
|
||||||
System paste validates UTF-8, unsupported controls, newlines, and a 4 MiB limit before editing; CRLF/CR normalize to LF. System exports also have a 4 MiB limit. Provider failures report an error without deleting/changing buffer text, caret/selection state, history, or repeat intent. No silent internal fallback occurs in System mode; choose Separate if clipboard access is unavailable. A native provider that fails after publishing may already have changed OS clipboard formats; vamp cannot roll that external side effect back.
|
System paste validates UTF-8, unsupported controls, newlines, and a 4 MiB limit before editing; CRLF/CR normalize to LF. System exports also have a 4 MiB limit. Provider failures report an error without deleting/changing buffer text, caret/selection state, history, or repeat intent. No silent internal fallback occurs in System mode; choose Separate if clipboard access is unavailable. A native provider that fails after publishing may already have changed OS clipboard formats; vamp cannot roll that external side effect back.
|
||||||
|
|
||||||
System clipboard access supports macOS through `osascript`, Linux Wayland through `wl-paste`/`wl-copy`, and Linux X11 through `xclip`. Linux tools must be on PATH with the corresponding display environment. Other platforms report unsupported access; choose Separate mode when system clipboard access is unavailable.
|
macOS copies publish UTF-8 plain text directly through the native pasteboard API, avoiding interpreter/process startup for every copy or cut; paste reads still use `osascript` with JXA/AppKit. Linux Wayland uses `wl-paste`/`wl-copy`, and Linux X11 uses `xclip`. Linux tools must be on PATH with the corresponding display environment. Other platforms report unsupported access; choose Separate mode when system clipboard access is unavailable.
|
||||||
|
|
||||||
## Repeat last edit
|
## Repeat last edit
|
||||||
|
|
||||||
|
|||||||
+187
-33
@@ -15,20 +15,6 @@ function run() {
|
|||||||
}
|
}
|
||||||
"#;
|
"#;
|
||||||
|
|
||||||
#[cfg(target_os = "macos")]
|
|
||||||
const MAC_WRITE: &str = r#"
|
|
||||||
ObjC.import('AppKit');
|
|
||||||
function run() {
|
|
||||||
var data = $.NSFileHandle.fileHandleWithStandardInput.readDataToEndOfFile;
|
|
||||||
var text = $.NSString.alloc.initWithDataEncoding(data, $.NSUTF8StringEncoding);
|
|
||||||
if (text.isNil()) throw Error('Clipboard input is not UTF-8');
|
|
||||||
var pasteboard = $.NSPasteboard.generalPasteboard;
|
|
||||||
pasteboard.clearContents;
|
|
||||||
if (!pasteboard.setStringForType(text, $.NSPasteboardTypeString))
|
|
||||||
throw Error('Cannot write plain-text clipboard');
|
|
||||||
}
|
|
||||||
"#;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
pub enum ClipboardMode {
|
pub enum ClipboardMode {
|
||||||
System,
|
System,
|
||||||
@@ -42,7 +28,7 @@ struct ClipboardCommand {
|
|||||||
|
|
||||||
struct Commands {
|
struct Commands {
|
||||||
reader: ClipboardCommand,
|
reader: ClipboardCommand,
|
||||||
writer: ClipboardCommand,
|
writer: Option<ClipboardCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SystemClipboard {
|
pub struct SystemClipboard {
|
||||||
@@ -72,10 +58,10 @@ impl SystemClipboard {
|
|||||||
program: reader,
|
program: reader,
|
||||||
args: &[],
|
args: &[],
|
||||||
},
|
},
|
||||||
writer: ClipboardCommand {
|
writer: Some(ClipboardCommand {
|
||||||
program: writer,
|
program: writer,
|
||||||
args: &[],
|
args: &[],
|
||||||
},
|
}),
|
||||||
})),
|
})),
|
||||||
timeout: COMMAND_TIMEOUT,
|
timeout: COMMAND_TIMEOUT,
|
||||||
}
|
}
|
||||||
@@ -95,8 +81,13 @@ impl SystemClipboard {
|
|||||||
}
|
}
|
||||||
let timeout = self.timeout;
|
let timeout = self.timeout;
|
||||||
let commands = self.commands.get_or_insert_with(select_commands);
|
let commands = self.commands.get_or_insert_with(select_commands);
|
||||||
let command = &commands.as_ref().map_err(Clone::clone)?.writer;
|
match &commands.as_ref().map_err(Clone::clone)?.writer {
|
||||||
run(command, Some(text.as_bytes()), timeout).map(|_| ())
|
Some(command) => run(command, Some(text.as_bytes()), timeout).map(|_| ()),
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
None => macos::write(text),
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
None => Err("System clipboard is unsupported on this platform".into()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,19 +108,13 @@ fn executable(name: &str) -> Option<PathBuf> {
|
|||||||
fn select_commands() -> Result<Commands, String> {
|
fn select_commands() -> Result<Commands, String> {
|
||||||
#[cfg(target_os = "macos")]
|
#[cfg(target_os = "macos")]
|
||||||
{
|
{
|
||||||
if let Some(program) = executable("osascript") {
|
Ok(Commands {
|
||||||
return Ok(Commands {
|
|
||||||
reader: ClipboardCommand {
|
reader: ClipboardCommand {
|
||||||
program: program.clone(),
|
program: executable("osascript").unwrap_or_else(|| "/usr/bin/osascript".into()),
|
||||||
args: &["-l", "JavaScript", "-e", MAC_READ],
|
args: &["-l", "JavaScript", "-e", MAC_READ],
|
||||||
},
|
},
|
||||||
writer: ClipboardCommand {
|
writer: None,
|
||||||
program,
|
})
|
||||||
args: &["-l", "JavaScript", "-e", MAC_WRITE],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err("System clipboard requires osascript with JXA/AppKit on PATH".into())
|
|
||||||
}
|
}
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
{
|
{
|
||||||
@@ -140,10 +125,10 @@ fn select_commands() -> Result<Commands, String> {
|
|||||||
program: reader,
|
program: reader,
|
||||||
args: &["--no-newline", "--type", "text"],
|
args: &["--no-newline", "--type", "text"],
|
||||||
},
|
},
|
||||||
writer: ClipboardCommand {
|
writer: Some(ClipboardCommand {
|
||||||
program: writer,
|
program: writer,
|
||||||
args: &["--type", "text/plain;charset=utf-8"],
|
args: &["--type", "text/plain;charset=utf-8"],
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -154,10 +139,10 @@ fn select_commands() -> Result<Commands, String> {
|
|||||||
program: program.clone(),
|
program: program.clone(),
|
||||||
args: &["-selection", "clipboard", "-target", "UTF8_STRING", "-out"],
|
args: &["-selection", "clipboard", "-target", "UTF8_STRING", "-out"],
|
||||||
},
|
},
|
||||||
writer: ClipboardCommand {
|
writer: Some(ClipboardCommand {
|
||||||
program,
|
program,
|
||||||
args: &["-selection", "clipboard", "-target", "UTF8_STRING", "-in"],
|
args: &["-selection", "clipboard", "-target", "UTF8_STRING", "-in"],
|
||||||
},
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +154,173 @@ fn select_commands() -> Result<Commands, String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod macos {
|
||||||
|
use std::ffi::{c_char, c_void};
|
||||||
|
use std::ptr;
|
||||||
|
|
||||||
|
type Object = *const c_void;
|
||||||
|
|
||||||
|
#[link(name = "CoreFoundation", kind = "framework")]
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn CFStringCreateWithCString(
|
||||||
|
allocator: Object,
|
||||||
|
bytes: *const c_char,
|
||||||
|
encoding: u32,
|
||||||
|
) -> Object;
|
||||||
|
fn CFDataCreate(allocator: Object, bytes: *const u8, length: isize) -> Object;
|
||||||
|
fn CFRelease(object: Object);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[link(name = "CoreServices", kind = "framework")]
|
||||||
|
unsafe extern "C" {
|
||||||
|
static kUTTypeUTF8PlainText: Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[link(name = "ApplicationServices", kind = "framework")]
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn PasteboardCreate(name: Object, pasteboard: *mut Object) -> i32;
|
||||||
|
fn PasteboardClear(pasteboard: Object) -> i32;
|
||||||
|
fn PasteboardPutItemFlavor(
|
||||||
|
pasteboard: Object,
|
||||||
|
item: usize,
|
||||||
|
flavor: Object,
|
||||||
|
data: Object,
|
||||||
|
flags: u32,
|
||||||
|
) -> i32;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Owned(Object);
|
||||||
|
impl Owned {
|
||||||
|
fn new(object: Object) -> Result<Self, String> {
|
||||||
|
if object.is_null() {
|
||||||
|
Err("Cannot allocate system clipboard data".into())
|
||||||
|
} else {
|
||||||
|
Ok(Self(object))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Owned {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// SAFETY: each handle owns one Core Foundation reference.
|
||||||
|
unsafe { CFRelease(self.0) };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check(status: i32) -> Result<(), String> {
|
||||||
|
if status == 0 {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(format!("System clipboard publication failed ({status})"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_to(name: Object, text: &str) -> Result<(), String> {
|
||||||
|
// SAFETY: framework constants and byte slices live throughout the calls.
|
||||||
|
unsafe {
|
||||||
|
let data = Owned::new(CFDataCreate(
|
||||||
|
ptr::null(),
|
||||||
|
text.as_ptr(),
|
||||||
|
text.len() as isize,
|
||||||
|
))?;
|
||||||
|
let mut board = ptr::null();
|
||||||
|
check(PasteboardCreate(name, &mut board))?;
|
||||||
|
let board = Owned::new(board)?;
|
||||||
|
// Prepare all allocations before changing clipboard contents.
|
||||||
|
check(PasteboardClear(board.0))?;
|
||||||
|
check(PasteboardPutItemFlavor(
|
||||||
|
board.0,
|
||||||
|
1,
|
||||||
|
kUTTypeUTF8PlainText,
|
||||||
|
data.0,
|
||||||
|
0,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn write(text: &str) -> Result<(), String> {
|
||||||
|
// SAFETY: the clipboard name is a process-lifetime, null-terminated literal.
|
||||||
|
let name = unsafe {
|
||||||
|
Owned::new(CFStringCreateWithCString(
|
||||||
|
ptr::null(),
|
||||||
|
c"com.apple.pasteboard.clipboard".as_ptr(),
|
||||||
|
0x0800_0100,
|
||||||
|
))?
|
||||||
|
};
|
||||||
|
write_to(name.0, text)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
unsafe extern "C" {
|
||||||
|
fn PasteboardSynchronize(pasteboard: Object) -> u32;
|
||||||
|
fn PasteboardGetItemIdentifier(
|
||||||
|
pasteboard: Object,
|
||||||
|
index: isize,
|
||||||
|
item: *mut usize,
|
||||||
|
) -> i32;
|
||||||
|
fn PasteboardCopyItemFlavorData(
|
||||||
|
pasteboard: Object,
|
||||||
|
item: usize,
|
||||||
|
flavor: Object,
|
||||||
|
data: *mut Object,
|
||||||
|
) -> i32;
|
||||||
|
fn CFDataGetBytePtr(data: Object) -> *const u8;
|
||||||
|
fn CFDataGetLength(data: Object) -> isize;
|
||||||
|
}
|
||||||
|
#[test]
|
||||||
|
fn native_publication_preserves_plain_utf8_bytes_on_an_isolated_pasteboard() {
|
||||||
|
// A private pasteboard leaves the user's clipboard untouched.
|
||||||
|
let name = unsafe {
|
||||||
|
Owned::new(CFStringCreateWithCString(
|
||||||
|
ptr::null(),
|
||||||
|
c"vamp-native-clipboard-regression".as_ptr(),
|
||||||
|
0x0800_0100,
|
||||||
|
))
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let board = unsafe {
|
||||||
|
let mut board = ptr::null();
|
||||||
|
check(PasteboardCreate(name.0, &mut board)).unwrap();
|
||||||
|
Owned::new(board).unwrap()
|
||||||
|
};
|
||||||
|
for text in [
|
||||||
|
"",
|
||||||
|
"é e\u{301} 👨💻\n\0tail",
|
||||||
|
"{\\rtf1 plain text}",
|
||||||
|
"%!PS-Adobe-3.0\nplain",
|
||||||
|
] {
|
||||||
|
write_to(name.0, text).unwrap();
|
||||||
|
unsafe {
|
||||||
|
PasteboardSynchronize(board.0);
|
||||||
|
let mut item = 0;
|
||||||
|
check(PasteboardGetItemIdentifier(board.0, 1, &mut item)).unwrap();
|
||||||
|
let flavor = Owned::new(CFStringCreateWithCString(
|
||||||
|
ptr::null(),
|
||||||
|
c"public.utf8-plain-text".as_ptr(),
|
||||||
|
0x0800_0100,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let mut data = ptr::null();
|
||||||
|
check(PasteboardCopyItemFlavorData(
|
||||||
|
board.0, item, flavor.0, &mut data,
|
||||||
|
))
|
||||||
|
.unwrap();
|
||||||
|
let data = Owned::new(data).unwrap();
|
||||||
|
let length = CFDataGetLength(data.0) as usize;
|
||||||
|
let bytes = if length == 0 {
|
||||||
|
&[]
|
||||||
|
} else {
|
||||||
|
std::slice::from_raw_parts(CFDataGetBytePtr(data.0), length)
|
||||||
|
};
|
||||||
|
assert_eq!(bytes, text.as_bytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||||
fn run(_: &ClipboardCommand, _: Option<&[u8]>, _: Duration) -> Result<Vec<u8>, String> {
|
fn run(_: &ClipboardCommand, _: Option<&[u8]>, _: Duration) -> Result<Vec<u8>, String> {
|
||||||
Err("System clipboard is unsupported on this platform".into())
|
Err("System clipboard is unsupported on this platform".into())
|
||||||
@@ -514,6 +666,8 @@ mod tests {
|
|||||||
.as_mut()
|
.as_mut()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.writer
|
.writer
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
.program = fixture.command(
|
.program = fixture.command(
|
||||||
"noisy",
|
"noisy",
|
||||||
&format!("/bin/dd if=/dev/zero bs=65536 count=8 >&2\nexec /bin/cat > {state}"),
|
&format!("/bin/dd if=/dev/zero bs=65536 count=8 >&2\nexec /bin/cat > {state}"),
|
||||||
|
|||||||
Reference in New Issue
Block a user