This PR introduces a new `MultiBufferOffset` new type wrapping size. The goal of this is to make it clear at the type level when we are interacting with offsets of a multi buffer versus offsets of a language / text buffer. This improves readability of things quite a bit by making it clear what kind of offsets one is working with while also reducing accidental bugs by using the wrong kin of offset for the wrong API. This PR also uncovered two minor bugs due to that. Does not yet introduce the MultiBufferPoint equivalent, that is for a follow up PR. Release Notes: - N/A *or* Added/Fixed/Improved ...
456 lines
14 KiB
Rust
456 lines
14 KiB
Rust
use editor::{ToOffset, movement};
|
|
use gpui::{Action, Context, Window};
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
|
|
use crate::{Vim, state::Mode};
|
|
|
|
/// Pastes text from the specified register at the cursor position.
|
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq, Action)]
|
|
#[action(namespace = vim)]
|
|
#[serde(deny_unknown_fields)]
|
|
pub struct HelixPaste {
|
|
#[serde(default)]
|
|
before: bool,
|
|
}
|
|
|
|
impl Vim {
|
|
pub fn helix_paste(
|
|
&mut self,
|
|
action: &HelixPaste,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
self.record_current_action(cx);
|
|
self.store_visual_marks(window, cx);
|
|
let count = Vim::take_count(cx).unwrap_or(1);
|
|
// TODO: vim paste calls take_forced_motion here, but I don't know what that does
|
|
// (none of the other helix_ methods call it)
|
|
|
|
self.update_editor(cx, |vim, editor, cx| {
|
|
editor.transact(window, cx, |editor, window, cx| {
|
|
editor.set_clip_at_line_ends(false, cx);
|
|
|
|
let selected_register = vim.selected_register.take();
|
|
|
|
let Some((text, clipboard_selections)) = Vim::update_globals(cx, |globals, cx| {
|
|
globals.read_register(selected_register, Some(editor), cx)
|
|
})
|
|
.and_then(|reg| {
|
|
(!reg.text.is_empty())
|
|
.then_some(reg.text)
|
|
.zip(reg.clipboard_selections)
|
|
}) else {
|
|
return;
|
|
};
|
|
|
|
let display_map = editor.display_snapshot(cx);
|
|
let current_selections = editor.selections.all_adjusted_display(&display_map);
|
|
|
|
// The clipboard can have multiple selections, and there can
|
|
// be multiple selections. Helix zips them together, so the first
|
|
// clipboard entry gets pasted at the first selection, the second
|
|
// entry gets pasted at the second selection, and so on. If there
|
|
// are more clipboard selections than selections, the extra ones
|
|
// don't get pasted anywhere. If there are more selections than
|
|
// clipboard selections, the last clipboard selection gets
|
|
// pasted at all remaining selections.
|
|
|
|
let mut edits = Vec::new();
|
|
let mut new_selections = Vec::new();
|
|
let mut start_offset = 0;
|
|
|
|
let mut replacement_texts: Vec<String> = Vec::new();
|
|
|
|
for ix in 0..current_selections.len() {
|
|
let to_insert = if let Some(clip_sel) = clipboard_selections.get(ix) {
|
|
let end_offset = start_offset + clip_sel.len;
|
|
let text = text[start_offset..end_offset].to_string();
|
|
start_offset = end_offset + 1;
|
|
text
|
|
} else if let Some(last_text) = replacement_texts.last() {
|
|
// We have more current selections than clipboard selections: repeat the last one.
|
|
last_text.to_owned()
|
|
} else {
|
|
text.to_string()
|
|
};
|
|
replacement_texts.push(to_insert);
|
|
}
|
|
|
|
let line_mode = replacement_texts.iter().any(|text| text.ends_with('\n'));
|
|
|
|
for (to_insert, sel) in replacement_texts.into_iter().zip(current_selections) {
|
|
// Helix doesn't care about the head/tail of the selection.
|
|
// Pasting before means pasting before the whole selection.
|
|
let display_point = if line_mode {
|
|
if action.before {
|
|
movement::line_beginning(&display_map, sel.start, false)
|
|
} else {
|
|
if sel.start == sel.end {
|
|
movement::right(
|
|
&display_map,
|
|
movement::line_end(&display_map, sel.end, false),
|
|
)
|
|
} else {
|
|
sel.end
|
|
}
|
|
}
|
|
} else if action.before {
|
|
sel.start
|
|
} else if sel.start == sel.end {
|
|
// Helix and Zed differ in how they understand
|
|
// single-point cursors. In Helix, a single-point cursor
|
|
// is "on top" of some character, and pasting after that
|
|
// cursor means that the pasted content should go after
|
|
// that character. (If the cursor is at the end of a
|
|
// line, the pasted content goes on the next line.)
|
|
movement::right(&display_map, sel.end)
|
|
} else {
|
|
sel.end
|
|
};
|
|
let point = display_point.to_point(&display_map);
|
|
let anchor = if action.before {
|
|
display_map.buffer_snapshot().anchor_after(point)
|
|
} else {
|
|
display_map.buffer_snapshot().anchor_before(point)
|
|
};
|
|
edits.push((point..point, to_insert.repeat(count)));
|
|
new_selections.push((anchor, to_insert.len() * count));
|
|
}
|
|
|
|
editor.edit(edits, cx);
|
|
|
|
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
|
editor.change_selections(Default::default(), window, cx, |s| {
|
|
s.select_ranges(new_selections.into_iter().map(|(anchor, len)| {
|
|
let offset = anchor.to_offset(&snapshot);
|
|
if action.before {
|
|
offset.saturating_sub_usize(len)..offset
|
|
} else {
|
|
offset..(offset + len)
|
|
}
|
|
}));
|
|
})
|
|
});
|
|
});
|
|
|
|
self.switch_mode(Mode::HelixNormal, true, window, cx);
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test {
|
|
use indoc::indoc;
|
|
|
|
use crate::{state::Mode, test::VimTestContext};
|
|
|
|
#[gpui::test]
|
|
async fn test_paste(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.enable_helix();
|
|
cx.set_state(
|
|
indoc! {"
|
|
The «quiˇ»ck brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.simulate_keystrokes("y w p");
|
|
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick «quiˇ»brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// Pasting before the selection:
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox «jumpsˇ» over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox «quiˇ»jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_point_selection_paste(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.enable_helix();
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quiˇck brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.simulate_keystrokes("y");
|
|
|
|
// Pasting before the selection:
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumpsˇ over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps«cˇ» over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// Pasting after the selection:
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumpsˇ over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps «cˇ»over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// Pasting after the selection at the end of a line:
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps overˇ
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
«cˇ»the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_multi_cursor_paste(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.enable_helix();
|
|
// Select two blocks of text.
|
|
cx.set_state(
|
|
indoc! {"
|
|
The «quiˇ»ck brown
|
|
fox ju«mpsˇ» over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("y");
|
|
|
|
// Only one cursor: only the first block gets pasted.
|
|
cx.set_state(
|
|
indoc! {"
|
|
ˇThe quick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«quiˇ»The quick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// Two cursors: both get pasted.
|
|
cx.set_state(
|
|
indoc! {"
|
|
ˇThe ˇquick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«quiˇ»The «mpsˇ»quick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// Three cursors: the second yanked block is duplicated.
|
|
cx.set_state(
|
|
indoc! {"
|
|
ˇThe ˇquick brown
|
|
fox jumpsˇ over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«quiˇ»The «mpsˇ»quick brown
|
|
fox jumps«mpsˇ» over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// Again with three cursors. All three should be pasted twice.
|
|
cx.set_state(
|
|
indoc! {"
|
|
ˇThe ˇquick brown
|
|
fox jumpsˇ over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("2 shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«quiquiˇ»The «mpsmpsˇ»quick brown
|
|
fox jumps«mpsmpsˇ» over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
}
|
|
|
|
#[gpui::test]
|
|
async fn test_line_mode_paste(cx: &mut gpui::TestAppContext) {
|
|
let mut cx = VimTestContext::new(cx, true).await;
|
|
cx.enable_helix();
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brow«n
|
|
ˇ»fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.simulate_keystrokes("y shift-p");
|
|
|
|
cx.assert_state(
|
|
indoc! {"
|
|
«n
|
|
ˇ»The quick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// In line mode, if we're in the middle of a line then pasting before pastes on
|
|
// the line before.
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumpsˇ over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("shift-p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
«n
|
|
ˇ»fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// In line mode, if we're in the middle of a line then pasting after pastes on
|
|
// the line after.
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumpsˇ over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
«n
|
|
ˇ»the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
// If we're currently at the end of a line, "the line after"
|
|
// means right after the cursor.
|
|
cx.set_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps overˇ
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
«n
|
|
ˇ»the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
|
|
The quick brown
|
|
fox jumps overˇ
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("x y up up p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
|
|
«fox jumps over
|
|
ˇ»The quick brown
|
|
fox jumps over
|
|
the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
|
|
cx.set_state(
|
|
indoc! {"
|
|
«The quick brown
|
|
fox jumps over
|
|
ˇ»the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
cx.simulate_keystrokes("y p p");
|
|
cx.assert_state(
|
|
indoc! {"
|
|
The quick brown
|
|
fox jumps over
|
|
The quick brown
|
|
fox jumps over
|
|
«The quick brown
|
|
fox jumps over
|
|
ˇ»the lazy dog."},
|
|
Mode::HelixNormal,
|
|
);
|
|
}
|
|
}
|