mirror of
https://github.com/microsoft/edit.git
synced 2025-07-03 14:33:22 +00:00
Large clipboard handling overhaul (#405)
* Make each paste its own undo step. * Add a `Paste` input type, allowing us to... * Fill the internal clipboard with bracketed paste contents. * Abstract away clipboard handling into its own struct, so we can move the cut/copy/paste logic into `TextBuffer`, allowing us to... * Implement smart line-wise copy/paste via Ctrl+C/Ctrl+V. Closes #286 Closes #305
This commit is contained in:
parent
a36fbe322d
commit
c5d91f301e
9 changed files with 210 additions and 120 deletions
|
@ -37,7 +37,7 @@ fn bench_buffer(c: &mut Criterion) {
|
||||||
{
|
{
|
||||||
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
||||||
tb.set_crlf(false);
|
tb.set_crlf(false);
|
||||||
tb.write(data.start_content.as_bytes(), true);
|
tb.write_raw(data.start_content.as_bytes());
|
||||||
|
|
||||||
for t in &data.txns {
|
for t in &data.txns {
|
||||||
for p in &t.patches {
|
for p in &t.patches {
|
||||||
|
@ -46,7 +46,7 @@ fn bench_buffer(c: &mut Criterion) {
|
||||||
|
|
||||||
tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType);
|
tb.delete(buffer::CursorMovement::Grapheme, p.1 as CoordType);
|
||||||
|
|
||||||
tb.write(p.2.as_bytes(), true);
|
tb.write_raw(p.2.as_bytes());
|
||||||
patches_with_coords.push((beg, p.1 as CoordType, p.2.clone()));
|
patches_with_coords.push((beg, p.1 as CoordType, p.2.clone()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,12 +72,12 @@ fn bench_buffer(c: &mut Criterion) {
|
||||||
let bench_text_buffer = || {
|
let bench_text_buffer = || {
|
||||||
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
let mut tb = buffer::TextBuffer::new(false).unwrap();
|
||||||
tb.set_crlf(false);
|
tb.set_crlf(false);
|
||||||
tb.write(data.start_content.as_bytes(), true);
|
tb.write_raw(data.start_content.as_bytes());
|
||||||
|
|
||||||
for p in &patches_with_coords {
|
for p in &patches_with_coords {
|
||||||
tb.cursor_move_to_logical(p.0);
|
tb.cursor_move_to_logical(p.0);
|
||||||
tb.delete(buffer::CursorMovement::Grapheme, p.1);
|
tb.delete(buffer::CursorMovement::Grapheme, p.1);
|
||||||
tb.write(p.2.as_bytes(), true);
|
tb.write_raw(p.2.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
tb
|
tb
|
||||||
|
|
|
@ -74,13 +74,15 @@ fn draw_menu_edit(ctx: &mut Context, state: &mut State) {
|
||||||
ctx.needs_rerender();
|
ctx.needs_rerender();
|
||||||
}
|
}
|
||||||
if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) {
|
if ctx.menubar_menu_button(loc(LocId::EditCut), 'T', kbmod::CTRL | vk::X) {
|
||||||
ctx.set_clipboard(tb.extract_selection(true));
|
tb.cut(ctx.clipboard_mut());
|
||||||
|
ctx.needs_rerender();
|
||||||
}
|
}
|
||||||
if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) {
|
if ctx.menubar_menu_button(loc(LocId::EditCopy), 'C', kbmod::CTRL | vk::C) {
|
||||||
ctx.set_clipboard(tb.extract_selection(false));
|
tb.copy(ctx.clipboard_mut());
|
||||||
|
ctx.needs_rerender();
|
||||||
}
|
}
|
||||||
if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) {
|
if ctx.menubar_menu_button(loc(LocId::EditPaste), 'P', kbmod::CTRL | vk::V) {
|
||||||
tb.write(ctx.clipboard(), true);
|
tb.paste(ctx.clipboard_ref());
|
||||||
ctx.needs_rerender();
|
ctx.needs_rerender();
|
||||||
}
|
}
|
||||||
if state.wants_search.kind != StateSearchKind::Disabled {
|
if state.wants_search.kind != StateSearchKind::Disabled {
|
||||||
|
|
|
@ -176,8 +176,8 @@ fn run() -> apperr::Result<()> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state.osc_clipboard_send_generation == tui.clipboard_generation() {
|
if state.osc_clipboard_sync {
|
||||||
write_osc_clipboard(&mut output, &mut state, &tui);
|
write_osc_clipboard(&mut tui, &mut state, &mut output);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "debug-latency")]
|
#[cfg(feature = "debug-latency")]
|
||||||
|
@ -317,7 +317,7 @@ fn draw(ctx: &mut Context, state: &mut State) {
|
||||||
if state.wants_about {
|
if state.wants_about {
|
||||||
draw_dialog_about(ctx, state);
|
draw_dialog_about(ctx, state);
|
||||||
}
|
}
|
||||||
if state.osc_clipboard_seen_generation != ctx.clipboard_generation() {
|
if ctx.clipboard_ref().wants_host_sync() {
|
||||||
draw_handle_clipboard_change(ctx, state);
|
draw_handle_clipboard_change(ctx, state);
|
||||||
}
|
}
|
||||||
if state.error_log_count != 0 {
|
if state.error_log_count != 0 {
|
||||||
|
@ -389,18 +389,19 @@ fn write_terminal_title(output: &mut ArenaString, filename: &str) {
|
||||||
output.push_str("edit\x1b\\");
|
output.push_str("edit\x1b\\");
|
||||||
}
|
}
|
||||||
|
|
||||||
const LARGE_CLIPBOARD_THRESHOLD: usize = 4 * KIBI;
|
const LARGE_CLIPBOARD_THRESHOLD: usize = 128 * KIBI;
|
||||||
|
|
||||||
fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
||||||
let generation = ctx.clipboard_generation();
|
let data_len = ctx.clipboard_ref().read().len();
|
||||||
|
|
||||||
if state.osc_clipboard_always_send || ctx.clipboard().len() < LARGE_CLIPBOARD_THRESHOLD {
|
if state.osc_clipboard_always_send || data_len < LARGE_CLIPBOARD_THRESHOLD {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
ctx.clipboard_mut().mark_as_synchronized();
|
||||||
state.osc_clipboard_send_generation = generation;
|
state.osc_clipboard_sync = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let over_limit = ctx.clipboard().len() >= SCRATCH_ARENA_CAPACITY / 4;
|
let over_limit = data_len >= SCRATCH_ARENA_CAPACITY / 4;
|
||||||
|
let mut done = None;
|
||||||
|
|
||||||
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
|
ctx.modal_begin("warning", loc(LocId::WarningDialogTitle));
|
||||||
{
|
{
|
||||||
|
@ -415,7 +416,7 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
||||||
} else {
|
} else {
|
||||||
let label2 = {
|
let label2 = {
|
||||||
let template = loc(LocId::LargeClipboardWarningLine2);
|
let template = loc(LocId::LargeClipboardWarningLine2);
|
||||||
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(ctx.clipboard().len()));
|
let size = arena_format!(ctx.arena(), "{}", MetricFormatter(data_len));
|
||||||
|
|
||||||
let mut label =
|
let mut label =
|
||||||
ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena());
|
ArenaString::with_capacity_in(template.len() + size.len(), ctx.arena());
|
||||||
|
@ -444,28 +445,26 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
||||||
|
|
||||||
if over_limit {
|
if over_limit {
|
||||||
if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
|
if ctx.button("ok", loc(LocId::Ok), ButtonStyle::default()) {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
done = Some(true);
|
||||||
}
|
}
|
||||||
ctx.inherit_focus();
|
ctx.inherit_focus();
|
||||||
} else {
|
} else {
|
||||||
if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) {
|
if ctx.button("always", loc(LocId::Always), ButtonStyle::default()) {
|
||||||
state.osc_clipboard_always_send = true;
|
state.osc_clipboard_always_send = true;
|
||||||
state.osc_clipboard_seen_generation = generation;
|
done = Some(true);
|
||||||
state.osc_clipboard_send_generation = generation;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) {
|
if ctx.button("yes", loc(LocId::Yes), ButtonStyle::default()) {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
done = Some(true);
|
||||||
state.osc_clipboard_send_generation = generation;
|
|
||||||
}
|
}
|
||||||
if ctx.clipboard().len() < 10 * LARGE_CLIPBOARD_THRESHOLD {
|
if data_len < 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||||
ctx.inherit_focus();
|
ctx.inherit_focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
|
if ctx.button("no", loc(LocId::No), ButtonStyle::default()) {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
done = Some(false);
|
||||||
}
|
}
|
||||||
if ctx.clipboard().len() >= 10 * LARGE_CLIPBOARD_THRESHOLD {
|
if data_len >= 10 * LARGE_CLIPBOARD_THRESHOLD {
|
||||||
ctx.inherit_focus();
|
ctx.inherit_focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -473,24 +472,33 @@ fn draw_handle_clipboard_change(ctx: &mut Context, state: &mut State) {
|
||||||
ctx.table_end();
|
ctx.table_end();
|
||||||
}
|
}
|
||||||
if ctx.modal_end() {
|
if ctx.modal_end() {
|
||||||
state.osc_clipboard_seen_generation = generation;
|
done = Some(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(sync) = done {
|
||||||
|
state.osc_clipboard_sync = sync;
|
||||||
|
ctx.clipboard_mut().mark_as_synchronized();
|
||||||
|
ctx.needs_rerender();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cold]
|
#[cold]
|
||||||
fn write_osc_clipboard(output: &mut ArenaString, state: &mut State, tui: &Tui) {
|
fn write_osc_clipboard(tui: &mut Tui, state: &mut State, output: &mut ArenaString) {
|
||||||
let clipboard = tui.clipboard();
|
let clipboard = tui.clipboard_mut();
|
||||||
if !clipboard.is_empty() {
|
let data = clipboard.read();
|
||||||
|
|
||||||
|
if !data.is_empty() {
|
||||||
// Rust doubles the size of a string when it needs to grow it.
|
// Rust doubles the size of a string when it needs to grow it.
|
||||||
// If `clipboard` is *really* large, this may then double
|
// If `data` is *really* large, this may then double
|
||||||
// the size of the `output` from e.g. 100MB to 200MB. Not good.
|
// the size of the `output` from e.g. 100MB to 200MB. Not good.
|
||||||
// We can avoid that by reserving the needed size in advance.
|
// We can avoid that by reserving the needed size in advance.
|
||||||
output.reserve_exact(base64::encode_len(clipboard.len()) + 16);
|
output.reserve_exact(base64::encode_len(data.len()) + 16);
|
||||||
output.push_str("\x1b]52;c;");
|
output.push_str("\x1b]52;c;");
|
||||||
base64::encode(output, clipboard);
|
base64::encode(output, data);
|
||||||
output.push_str("\x1b\\");
|
output.push_str("\x1b\\");
|
||||||
}
|
}
|
||||||
state.osc_clipboard_send_generation = tui.clipboard_generation().wrapping_sub(1);
|
|
||||||
|
state.osc_clipboard_sync = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RestoreModes;
|
struct RestoreModes;
|
||||||
|
|
|
@ -162,8 +162,7 @@ pub struct State {
|
||||||
pub goto_invalid: bool,
|
pub goto_invalid: bool,
|
||||||
|
|
||||||
pub osc_title_filename: String,
|
pub osc_title_filename: String,
|
||||||
pub osc_clipboard_seen_generation: u32,
|
pub osc_clipboard_sync: bool,
|
||||||
pub osc_clipboard_send_generation: u32,
|
|
||||||
pub osc_clipboard_always_send: bool,
|
pub osc_clipboard_always_send: bool,
|
||||||
pub exit: bool,
|
pub exit: bool,
|
||||||
}
|
}
|
||||||
|
@ -211,8 +210,7 @@ impl State {
|
||||||
goto_invalid: false,
|
goto_invalid: false,
|
||||||
|
|
||||||
osc_title_filename: Default::default(),
|
osc_title_filename: Default::default(),
|
||||||
osc_clipboard_seen_generation: 0,
|
osc_clipboard_sync: false,
|
||||||
osc_clipboard_send_generation: 0,
|
|
||||||
osc_clipboard_always_send: false,
|
osc_clipboard_always_send: false,
|
||||||
exit: false,
|
exit: false,
|
||||||
})
|
})
|
||||||
|
|
|
@ -38,6 +38,7 @@ pub use gap_buffer::GapBuffer;
|
||||||
|
|
||||||
use crate::arena::{ArenaString, scratch_arena};
|
use crate::arena::{ArenaString, scratch_arena};
|
||||||
use crate::cell::SemiRefCell;
|
use crate::cell::SemiRefCell;
|
||||||
|
use crate::clipboard::Clipboard;
|
||||||
use crate::document::{ReadableDocument, WriteableDocument};
|
use crate::document::{ReadableDocument, WriteableDocument};
|
||||||
use crate::framebuffer::{Framebuffer, IndexedColor};
|
use crate::framebuffer::{Framebuffer, IndexedColor};
|
||||||
use crate::helpers::*;
|
use crate::helpers::*;
|
||||||
|
@ -1083,7 +1084,7 @@ impl TextBuffer {
|
||||||
if let (Some(search), Some(..)) = (&mut self.search, &self.selection) {
|
if let (Some(search), Some(..)) = (&mut self.search, &self.selection) {
|
||||||
let search = search.get_mut();
|
let search = search.get_mut();
|
||||||
if search.selection_generation == self.selection_generation {
|
if search.selection_generation == self.selection_generation {
|
||||||
self.write(replacement.as_bytes(), true);
|
self.write(replacement.as_bytes(), self.cursor, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1106,7 +1107,7 @@ impl TextBuffer {
|
||||||
if !self.has_selection() {
|
if !self.has_selection() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
self.write(replacement, true);
|
self.write(replacement, self.cursor, true);
|
||||||
offset = self.cursor.offset;
|
offset = self.cursor.offset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1822,15 +1823,60 @@ impl TextBuffer {
|
||||||
Some(RenderResult { visual_pos_x_max })
|
Some(RenderResult { visual_pos_x_max })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Inserts `text` at the current cursor position.
|
pub fn cut(&mut self, clipboard: &mut Clipboard) {
|
||||||
///
|
self.cut_copy(clipboard, true);
|
||||||
/// If there's a current selection, it will be replaced.
|
}
|
||||||
/// The selection is cleared after the call.
|
|
||||||
pub fn write(&mut self, text: &[u8], raw: bool) {
|
pub fn copy(&mut self, clipboard: &mut Clipboard) {
|
||||||
|
self.cut_copy(clipboard, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cut_copy(&mut self, clipboard: &mut Clipboard, cut: bool) {
|
||||||
|
let line_copy = !self.has_selection();
|
||||||
|
let selection = self.extract_selection(cut);
|
||||||
|
clipboard.write(selection);
|
||||||
|
clipboard.write_was_line_copy(line_copy);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn paste(&mut self, clipboard: &Clipboard) {
|
||||||
|
let data = clipboard.read();
|
||||||
|
if data.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = self.cursor_logical_pos();
|
||||||
|
let at = if clipboard.is_line_copy() {
|
||||||
|
self.goto_line_start(self.cursor, pos.y)
|
||||||
|
} else {
|
||||||
|
self.cursor
|
||||||
|
};
|
||||||
|
|
||||||
|
self.write(data, at, true);
|
||||||
|
|
||||||
|
if clipboard.is_line_copy() {
|
||||||
|
self.cursor_move_to_logical(Point { x: pos.x, y: pos.y + 1 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts the user input `text` at the current cursor position.
|
||||||
|
/// Replaces tabs with whitespace if needed, etc.
|
||||||
|
pub fn write_canon(&mut self, text: &[u8]) {
|
||||||
|
self.write(text, self.cursor, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Inserts `text` as-is at the current cursor position.
|
||||||
|
/// The only transformation applied is that newlines are normalized.
|
||||||
|
pub fn write_raw(&mut self, text: &[u8]) {
|
||||||
|
self.write(text, self.cursor, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write(&mut self, text: &[u8], at: Cursor, raw: bool) {
|
||||||
|
let history_type = if raw { HistoryType::Other } else { HistoryType::Write };
|
||||||
|
|
||||||
// If we have an active selection, writing an empty `text`
|
// If we have an active selection, writing an empty `text`
|
||||||
// will still delete the selection. As such, we check this first.
|
// will still delete the selection. As such, we check this first.
|
||||||
if let Some((beg, end)) = self.selection_range_internal(false) {
|
if let Some((beg, end)) = self.selection_range_internal(false) {
|
||||||
self.edit_begin(HistoryType::Write, beg);
|
self.edit_begin(history_type, beg);
|
||||||
self.edit_delete(end);
|
self.edit_delete(end);
|
||||||
self.set_selection(None);
|
self.set_selection(None);
|
||||||
}
|
}
|
||||||
|
@ -1846,7 +1892,7 @@ impl TextBuffer {
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.active_edit_depth <= 0 {
|
if self.active_edit_depth <= 0 {
|
||||||
self.edit_begin(HistoryType::Write, self.cursor);
|
self.edit_begin(history_type, at);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
|
@ -2125,7 +2171,8 @@ impl TextBuffer {
|
||||||
|
|
||||||
/// Extracts the contents of the current selection.
|
/// Extracts the contents of the current selection.
|
||||||
/// May optionally delete it, if requested. This is meant to be used for Ctrl+X.
|
/// May optionally delete it, if requested. This is meant to be used for Ctrl+X.
|
||||||
pub fn extract_selection(&mut self, delete: bool) -> Vec<u8> {
|
fn extract_selection(&mut self, delete: bool) -> Vec<u8> {
|
||||||
|
let line_copy = !self.has_selection();
|
||||||
let Some((beg, end)) = self.selection_range_internal(true) else {
|
let Some((beg, end)) = self.selection_range_internal(true) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
|
@ -2140,6 +2187,11 @@ impl TextBuffer {
|
||||||
self.set_selection(None);
|
self.set_selection(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Line copies (= Ctrl+C when there's no selection) always end with a newline.
|
||||||
|
if line_copy && !out.ends_with(b"\n") {
|
||||||
|
out.replace_range(out.len().., if self.newlines_are_crlf { b"\r\n" } else { b"\n" });
|
||||||
|
}
|
||||||
|
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
53
src/clipboard.rs
Normal file
53
src/clipboard.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
//! Clipboard facilities for the editor.
|
||||||
|
|
||||||
|
/// The builtin, internal clipboard of the editor.
|
||||||
|
///
|
||||||
|
/// This is useful particularly when the terminal doesn't support
|
||||||
|
/// OSC 52 or when the clipboard contents are huge (e.g. 1GiB).
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Clipboard {
|
||||||
|
data: Vec<u8>,
|
||||||
|
line_copy: bool,
|
||||||
|
wants_host_sync: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clipboard {
|
||||||
|
/// If true, we should emit a OSC 52 sequence to sync the clipboard
|
||||||
|
/// with the hosting terminal.
|
||||||
|
pub fn wants_host_sync(&self) -> bool {
|
||||||
|
self.wants_host_sync
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call this once the clipboard has been synchronized with the host.
|
||||||
|
pub fn mark_as_synchronized(&mut self) {
|
||||||
|
self.wants_host_sync = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The editor has a special behavior when you have no selection and press
|
||||||
|
/// Ctrl+C: It copies the current line to the clipboard. Then, when you
|
||||||
|
/// paste it, it inserts the line at *the start* of the current line.
|
||||||
|
/// This effectively prepends the current line with the copied line.
|
||||||
|
/// `clipboard_line_start` is true in that case.
|
||||||
|
pub fn is_line_copy(&self) -> bool {
|
||||||
|
self.line_copy
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the current contents of the clipboard.
|
||||||
|
pub fn read(&self) -> &[u8] {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fill the clipboard with the given data.
|
||||||
|
pub fn write(&mut self, data: Vec<u8>) {
|
||||||
|
if !data.is_empty() {
|
||||||
|
self.data = data;
|
||||||
|
self.line_copy = false;
|
||||||
|
self.wants_host_sync = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// See [`Clipboard::is_line_copy`].
|
||||||
|
pub fn write_was_line_copy(&mut self, line_copy: bool) {
|
||||||
|
self.line_copy = line_copy;
|
||||||
|
}
|
||||||
|
}
|
30
src/input.rs
30
src/input.rs
|
@ -6,6 +6,8 @@
|
||||||
//! In the future this allows us to take apart the application and
|
//! In the future this allows us to take apart the application and
|
||||||
//! support input schemes that aren't VT, such as UEFI, or GUI.
|
//! support input schemes that aren't VT, such as UEFI, or GUI.
|
||||||
|
|
||||||
|
use std::mem;
|
||||||
|
|
||||||
use crate::helpers::{CoordType, Point, Size};
|
use crate::helpers::{CoordType, Point, Size};
|
||||||
use crate::vt;
|
use crate::vt;
|
||||||
|
|
||||||
|
@ -217,16 +219,6 @@ pub mod kbmod {
|
||||||
pub const CTRL_ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x07000000);
|
pub const CTRL_ALT_SHIFT: InputKeyMod = InputKeyMod::new(0x07000000);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Text input.
|
|
||||||
///
|
|
||||||
/// "Keyboard" input is also "text" input and vice versa.
|
|
||||||
/// It differs in that text input can also be Unicode.
|
|
||||||
#[derive(Clone, Copy)]
|
|
||||||
pub struct InputText<'a> {
|
|
||||||
pub text: &'a str,
|
|
||||||
pub bracketed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mouse input state. Up/Down, Left/Right, etc.
|
/// Mouse input state. Up/Down, Left/Right, etc.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||||
pub enum InputMouseState {
|
pub enum InputMouseState {
|
||||||
|
@ -261,9 +253,10 @@ pub enum Input<'input> {
|
||||||
/// Window resize event.
|
/// Window resize event.
|
||||||
Resize(Size),
|
Resize(Size),
|
||||||
/// Text input.
|
/// Text input.
|
||||||
///
|
|
||||||
/// Note that [`Input::Keyboard`] events can also be text.
|
/// Note that [`Input::Keyboard`] events can also be text.
|
||||||
Text(InputText<'input>),
|
Text(&'input str),
|
||||||
|
/// A clipboard paste.
|
||||||
|
Paste(Vec<u8>),
|
||||||
/// Keyboard input.
|
/// Keyboard input.
|
||||||
Keyboard(InputKey),
|
Keyboard(InputKey),
|
||||||
/// Mouse input.
|
/// Mouse input.
|
||||||
|
@ -273,6 +266,7 @@ pub enum Input<'input> {
|
||||||
/// Parses VT sequences into input events.
|
/// Parses VT sequences into input events.
|
||||||
pub struct Parser {
|
pub struct Parser {
|
||||||
bracketed_paste: bool,
|
bracketed_paste: bool,
|
||||||
|
bracketed_paste_buf: Vec<u8>,
|
||||||
x10_mouse_want: bool,
|
x10_mouse_want: bool,
|
||||||
x10_mouse_buf: [u8; 3],
|
x10_mouse_buf: [u8; 3],
|
||||||
x10_mouse_len: usize,
|
x10_mouse_len: usize,
|
||||||
|
@ -285,6 +279,7 @@ impl Parser {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
bracketed_paste: false,
|
bracketed_paste: false,
|
||||||
|
bracketed_paste_buf: Vec::new(),
|
||||||
x10_mouse_want: false,
|
x10_mouse_want: false,
|
||||||
x10_mouse_buf: [0; 3],
|
x10_mouse_buf: [0; 3],
|
||||||
x10_mouse_len: 0,
|
x10_mouse_len: 0,
|
||||||
|
@ -333,7 +328,7 @@ impl<'input> Iterator for Stream<'_, '_, 'input> {
|
||||||
|
|
||||||
match self.stream.next()? {
|
match self.stream.next()? {
|
||||||
vt::Token::Text(text) => {
|
vt::Token::Text(text) => {
|
||||||
return Some(Input::Text(InputText { text, bracketed: false }));
|
return Some(Input::Text(text));
|
||||||
}
|
}
|
||||||
vt::Token::Ctrl(ch) => match ch {
|
vt::Token::Ctrl(ch) => match ch {
|
||||||
'\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))),
|
'\0' | '\t' | '\r' => return Some(Input::Keyboard(InputKey::new(ch as u32))),
|
||||||
|
@ -519,8 +514,13 @@ impl<'input> Stream<'_, '_, 'input> {
|
||||||
}
|
}
|
||||||
|
|
||||||
if end != beg {
|
if end != beg {
|
||||||
let input = self.stream.input();
|
self.parser
|
||||||
Some(Input::Text(InputText { text: &input[beg..end], bracketed: true }))
|
.bracketed_paste_buf
|
||||||
|
.extend_from_slice(&self.stream.input().as_bytes()[beg..end]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if !self.parser.bracketed_paste {
|
||||||
|
Some(Input::Paste(mem::take(&mut self.parser.bracketed_paste_buf)))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ pub mod apperr;
|
||||||
pub mod base64;
|
pub mod base64;
|
||||||
pub mod buffer;
|
pub mod buffer;
|
||||||
pub mod cell;
|
pub mod cell;
|
||||||
|
pub mod clipboard;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod framebuffer;
|
pub mod framebuffer;
|
||||||
pub mod fuzzy;
|
pub mod fuzzy;
|
||||||
|
|
92
src/tui.rs
92
src/tui.rs
|
@ -152,6 +152,7 @@ use std::{iter, mem, ptr, time};
|
||||||
use crate::arena::{Arena, ArenaString, scratch_arena};
|
use crate::arena::{Arena, ArenaString, scratch_arena};
|
||||||
use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell};
|
use crate::buffer::{CursorMovement, RcTextBuffer, TextBuffer, TextBufferCell};
|
||||||
use crate::cell::*;
|
use crate::cell::*;
|
||||||
|
use crate::clipboard::Clipboard;
|
||||||
use crate::document::WriteableDocument;
|
use crate::document::WriteableDocument;
|
||||||
use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor};
|
use crate::framebuffer::{Attributes, Framebuffer, INDEXED_COLORS_COUNT, IndexedColor};
|
||||||
use crate::hash::*;
|
use crate::hash::*;
|
||||||
|
@ -167,7 +168,6 @@ const KBMOD_FOR_WORD_NAV: InputKeyMod =
|
||||||
type Input<'input> = input::Input<'input>;
|
type Input<'input> = input::Input<'input>;
|
||||||
type InputKey = input::InputKey;
|
type InputKey = input::InputKey;
|
||||||
type InputMouseState = input::InputMouseState;
|
type InputMouseState = input::InputMouseState;
|
||||||
type InputText<'input> = input::InputText<'input>;
|
|
||||||
|
|
||||||
/// Since [`TextBuffer`] creation and management is expensive,
|
/// Since [`TextBuffer`] creation and management is expensive,
|
||||||
/// we cache instances of them for reuse between frames.
|
/// we cache instances of them for reuse between frames.
|
||||||
|
@ -363,10 +363,7 @@ pub struct Tui {
|
||||||
cached_text_buffers: Vec<CachedTextBuffer>,
|
cached_text_buffers: Vec<CachedTextBuffer>,
|
||||||
|
|
||||||
/// The clipboard contents.
|
/// The clipboard contents.
|
||||||
clipboard: Vec<u8>,
|
clipboard: Clipboard,
|
||||||
/// A counter that is incremented every time the clipboard changes.
|
|
||||||
/// Allows for tracking clipboard changes without comparing contents.
|
|
||||||
clipboard_generation: u32,
|
|
||||||
|
|
||||||
settling_have: i32,
|
settling_have: i32,
|
||||||
settling_want: i32,
|
settling_want: i32,
|
||||||
|
@ -416,8 +413,7 @@ impl Tui {
|
||||||
|
|
||||||
cached_text_buffers: Vec::with_capacity(16),
|
cached_text_buffers: Vec::with_capacity(16),
|
||||||
|
|
||||||
clipboard: Vec::new(),
|
clipboard: Default::default(),
|
||||||
clipboard_generation: 0,
|
|
||||||
|
|
||||||
settling_have: 0,
|
settling_have: 0,
|
||||||
settling_want: 0,
|
settling_want: 0,
|
||||||
|
@ -490,16 +486,14 @@ impl Tui {
|
||||||
self.framebuffer.contrasted(color)
|
self.framebuffer.contrasted(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current clipboard contents.
|
/// Returns the clipboard.
|
||||||
pub fn clipboard(&self) -> &[u8] {
|
pub fn clipboard_ref(&self) -> &Clipboard {
|
||||||
&self.clipboard
|
&self.clipboard
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current clipboard generation.
|
/// Returns the clipboard (mutable).
|
||||||
/// The generation changes every time the clipboard contents change.
|
pub fn clipboard_mut(&mut self) -> &mut Clipboard {
|
||||||
/// This allows you to track clipboard changes.
|
&mut self.clipboard
|
||||||
pub fn clipboard_generation(&self) -> u32 {
|
|
||||||
self.clipboard_generation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Starts a new frame and returns a [`Context`] for it.
|
/// Starts a new frame and returns a [`Context`] for it.
|
||||||
|
@ -553,11 +547,17 @@ impl Tui {
|
||||||
// This causes us to ignore the keyboard input here. We need a way to inform the caller over
|
// This causes us to ignore the keyboard input here. We need a way to inform the caller over
|
||||||
// how much of the input text we actually processed in a single frame. Or perhaps we could use
|
// how much of the input text we actually processed in a single frame. Or perhaps we could use
|
||||||
// the needs_settling logic?
|
// the needs_settling logic?
|
||||||
if !text.bracketed && text.text.len() == 1 {
|
if text.len() == 1 {
|
||||||
let ch = text.text.as_bytes()[0];
|
let ch = text.as_bytes()[0];
|
||||||
input_keyboard = InputKey::from_ascii(ch as char)
|
input_keyboard = InputKey::from_ascii(ch as char)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Some(Input::Paste(paste)) => {
|
||||||
|
let clipboard = self.clipboard_mut();
|
||||||
|
clipboard.write(paste);
|
||||||
|
clipboard.mark_as_synchronized();
|
||||||
|
input_keyboard = Some(kbmod::CTRL | vk::V);
|
||||||
|
}
|
||||||
Some(Input::Keyboard(keyboard)) => {
|
Some(Input::Keyboard(keyboard)) => {
|
||||||
input_keyboard = Some(keyboard);
|
input_keyboard = Some(keyboard);
|
||||||
}
|
}
|
||||||
|
@ -1314,7 +1314,7 @@ pub struct Context<'a, 'input> {
|
||||||
tui: &'a mut Tui,
|
tui: &'a mut Tui,
|
||||||
|
|
||||||
/// Current text input, if any.
|
/// Current text input, if any.
|
||||||
input_text: Option<InputText<'input>>,
|
input_text: Option<&'input str>,
|
||||||
/// Current keyboard input, if any.
|
/// Current keyboard input, if any.
|
||||||
input_keyboard: Option<InputKey>,
|
input_keyboard: Option<InputKey>,
|
||||||
input_mouse_modifiers: InputKeyMod,
|
input_mouse_modifiers: InputKeyMod,
|
||||||
|
@ -1376,25 +1376,14 @@ impl<'a> Context<'a, '_> {
|
||||||
self.tui.framebuffer.contrasted(color)
|
self.tui.framebuffer.contrasted(color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current clipboard contents.
|
/// Returns the clipboard.
|
||||||
pub fn clipboard(&self) -> &[u8] {
|
pub fn clipboard_ref(&self) -> &Clipboard {
|
||||||
self.tui.clipboard()
|
&self.tui.clipboard
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current clipboard generation.
|
/// Returns the clipboard (mutable).
|
||||||
/// The generation changes every time the clipboard contents change.
|
pub fn clipboard_mut(&mut self) -> &mut Clipboard {
|
||||||
/// This allows you to track clipboard changes.
|
&mut self.tui.clipboard
|
||||||
pub fn clipboard_generation(&self) -> u32 {
|
|
||||||
self.tui.clipboard_generation()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sets the clipboard contents.
|
|
||||||
pub fn set_clipboard(&mut self, data: Vec<u8>) {
|
|
||||||
if !data.is_empty() {
|
|
||||||
self.tui.clipboard = data;
|
|
||||||
self.tui.clipboard_generation = self.tui.clipboard_generation.wrapping_add(1);
|
|
||||||
self.needs_rerender();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tell the UI framework that your state changed and you need another layout pass.
|
/// Tell the UI framework that your state changed and you need another layout pass.
|
||||||
|
@ -2052,11 +2041,7 @@ impl<'a> Context<'a, '_> {
|
||||||
|
|
||||||
/// Creates a text input field.
|
/// Creates a text input field.
|
||||||
/// Returns true if the text contents changed.
|
/// Returns true if the text contents changed.
|
||||||
pub fn editline<'s, 'b: 's>(
|
pub fn editline(&mut self, classname: &'static str, text: &mut dyn WriteableDocument) -> bool {
|
||||||
&'s mut self,
|
|
||||||
classname: &'static str,
|
|
||||||
text: &'b mut dyn WriteableDocument,
|
|
||||||
) -> bool {
|
|
||||||
self.textarea_internal(classname, TextBufferPayload::Editline(text))
|
self.textarea_internal(classname, TextBufferPayload::Editline(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2322,14 +2307,10 @@ impl<'a> Context<'a, '_> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut write: &[u8] = b"";
|
let mut write: &[u8] = &[];
|
||||||
let mut write_raw = false;
|
|
||||||
|
|
||||||
if let Some(input) = &self.input_text {
|
if let Some(input) = &self.input_text {
|
||||||
write = input.text.as_bytes();
|
write = input.as_bytes();
|
||||||
write_raw = input.bracketed;
|
|
||||||
tc.preferred_column = tb.cursor_visual_pos().x;
|
|
||||||
make_cursor_visible = true;
|
|
||||||
} else if let Some(input) = &self.input_keyboard {
|
} else if let Some(input) = &self.input_keyboard {
|
||||||
let key = input.key();
|
let key = input.key();
|
||||||
let modifiers = input.modifiers();
|
let modifiers = input.modifiers();
|
||||||
|
@ -2643,15 +2624,12 @@ impl<'a> Context<'a, '_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
vk::INSERT => match modifiers {
|
vk::INSERT => match modifiers {
|
||||||
kbmod::SHIFT => {
|
kbmod::SHIFT => tb.paste(self.clipboard_ref()),
|
||||||
write = &self.tui.clipboard;
|
kbmod::CTRL => tb.copy(self.clipboard_mut()),
|
||||||
write_raw = true;
|
|
||||||
}
|
|
||||||
kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)),
|
|
||||||
_ => tb.set_overtype(!tb.is_overtype()),
|
_ => tb.set_overtype(!tb.is_overtype()),
|
||||||
},
|
},
|
||||||
vk::DELETE => match modifiers {
|
vk::DELETE => match modifiers {
|
||||||
kbmod::SHIFT => self.set_clipboard(tb.extract_selection(true)),
|
kbmod::SHIFT => tb.cut(self.clipboard_mut()),
|
||||||
kbmod::CTRL => tb.delete(CursorMovement::Word, 1),
|
kbmod::CTRL => tb.delete(CursorMovement::Word, 1),
|
||||||
_ => tb.delete(CursorMovement::Grapheme, 1),
|
_ => tb.delete(CursorMovement::Grapheme, 1),
|
||||||
},
|
},
|
||||||
|
@ -2680,18 +2658,15 @@ impl<'a> Context<'a, '_> {
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
vk::X => match modifiers {
|
vk::X => match modifiers {
|
||||||
kbmod::CTRL => self.set_clipboard(tb.extract_selection(true)),
|
kbmod::CTRL => tb.cut(self.clipboard_mut()),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
vk::C => match modifiers {
|
vk::C => match modifiers {
|
||||||
kbmod::CTRL => self.set_clipboard(tb.extract_selection(false)),
|
kbmod::CTRL => tb.copy(self.clipboard_mut()),
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
vk::V => match modifiers {
|
vk::V => match modifiers {
|
||||||
kbmod::CTRL => {
|
kbmod::CTRL => tb.paste(self.clipboard_ref()),
|
||||||
write = &self.tui.clipboard;
|
|
||||||
write_raw = true;
|
|
||||||
}
|
|
||||||
_ => return false,
|
_ => return false,
|
||||||
},
|
},
|
||||||
vk::Y => match modifiers {
|
vk::Y => match modifiers {
|
||||||
|
@ -2717,8 +2692,9 @@ impl<'a> Context<'a, '_> {
|
||||||
write = unicode::strip_newline(&write[..end]);
|
write = unicode::strip_newline(&write[..end]);
|
||||||
}
|
}
|
||||||
if !write.is_empty() {
|
if !write.is_empty() {
|
||||||
tb.write(write, write_raw);
|
tb.write_canon(write);
|
||||||
change_preferred_column = true;
|
change_preferred_column = true;
|
||||||
|
make_cursor_visible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if change_preferred_column {
|
if change_preferred_column {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue