-
-
Notifications
You must be signed in to change notification settings - Fork 2.9k
feat: New command :paste-join
#13600
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
d8dabfb
401bdd0
fe4202e
8b8a731
ae16bbb
9e72279
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,6 +74,7 @@ use std::{ | |
future::Future, | ||
io::Read, | ||
num::NonZeroUsize, | ||
str::FromStr, | ||
}; | ||
|
||
use std::{ | ||
|
@@ -214,6 +215,10 @@ pub enum MappableCommand { | |
}, | ||
Static { | ||
name: &'static str, | ||
// TODO: Change the signature to | ||
// fn(cx: &mut Context) -> anyhow::Result<()> | ||
// | ||
// Then handle the error by using `Editor::set_error` in a single place | ||
fun: fn(cx: &mut Context), | ||
doc: &'static str, | ||
}, | ||
|
@@ -494,6 +499,9 @@ impl MappableCommand { | |
paste_clipboard_before, "Paste clipboard before selections", | ||
paste_primary_clipboard_after, "Paste primary clipboard after selections", | ||
paste_primary_clipboard_before, "Paste primary clipboard before selections", | ||
paste_before_joined_with_newline, "Join all selections with a newline and paste before cursor", | ||
paste_after_joined_with_newline, "Join all selections with a newline and paste after cursor", | ||
replace_joined_with_newline, "Replace selection with all selections joined with a newline", | ||
indent, "Indent selection", | ||
unindent, "Unindent selection", | ||
format_selections, "Format selection", | ||
|
@@ -4620,6 +4628,35 @@ enum Paste { | |
Cursor, | ||
} | ||
|
||
/// Where to paste joined selections | ||
#[derive(Copy, Clone, Default)] | ||
pub enum PasteJoined { | ||
/// Paste before the cursor | ||
Before, | ||
/// Paste after the cursor | ||
#[default] | ||
After, | ||
/// Replace the selection with cursor | ||
Replace, | ||
} | ||
|
||
impl PasteJoined { | ||
const VARIANTS: [&'static str; 3] = ["before", "after", "replace"]; | ||
} | ||
|
||
impl FromStr for PasteJoined { | ||
type Err = anyhow::Error; | ||
|
||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
match s { | ||
"before" => Ok(Self::Before), | ||
"after" => Ok(Self::After), | ||
"replace" => Ok(Self::Replace), | ||
_ => Err(anyhow!("Invalid paste position: {s}")), | ||
} | ||
} | ||
} | ||
|
||
static LINE_ENDING_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"\r\n|\r|\n").unwrap()); | ||
|
||
fn paste_impl( | ||
|
@@ -4747,32 +4784,38 @@ fn replace_with_yanked(cx: &mut Context) { | |
} | ||
|
||
fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) { | ||
let Some(values) = editor | ||
.registers | ||
.read(register, editor) | ||
.filter(|values| values.len() > 0) | ||
else { | ||
let scrolloff = editor.config().scrolloff; | ||
|
||
let Some(values) = editor.registers.read(register, editor) else { | ||
return; | ||
}; | ||
let scrolloff = editor.config().scrolloff; | ||
let (view, doc) = current_ref!(editor); | ||
let yanked = values.map(|value| value.to_string()).collect::<Vec<_>>(); | ||
let (view, doc) = current!(editor); | ||
|
||
let map_value = |value: &Cow<str>| { | ||
replace_impl(&yanked, doc, view, count, scrolloff) | ||
} | ||
|
||
fn replace_impl( | ||
values: &[String], | ||
doc: &mut Document, | ||
view: &mut View, | ||
count: usize, | ||
scrolloff: usize, | ||
) { | ||
let map_value = |value: &String| { | ||
let value = LINE_ENDING_REGEX.replace_all(value, doc.line_ending.as_str()); | ||
let mut out = Tendril::from(value.as_ref()); | ||
for _ in 1..count { | ||
out.push_str(&value); | ||
} | ||
out | ||
}; | ||
let mut values_rev = values.rev().peekable(); | ||
// `values` is asserted to have at least one entry above. | ||
let last = values_rev.peek().unwrap(); | ||
let mut values_rev = values.iter().rev().peekable(); | ||
let Some(last) = values_rev.peek() else { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why did you change this from an unwrap? As far as I can see there is still always atleast one value? And if there isn't I would rather unwrap instead of silently doing nothing (or atleast log an error) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Previously we had But now, I've moved The effect is the same as before: Do nothing if it's empty. It can be empty when the register contains nothing. E.g., This behaviour (including not logging anything) is consistent with what would happen if you did |
||
return; | ||
}; | ||
let repeat = std::iter::repeat(map_value(last)); | ||
let mut values = values_rev | ||
.rev() | ||
.map(|value| map_value(&value)) | ||
.chain(repeat); | ||
let mut values = values_rev.rev().map(map_value).chain(repeat); | ||
let selection = doc.selection(view.id); | ||
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { | ||
if !range.is_empty() { | ||
|
@@ -4781,9 +4824,7 @@ fn replace_with_yanked_impl(editor: &mut Editor, register: char, count: usize) { | |
(range.from(), range.to(), None) | ||
} | ||
}); | ||
drop(values); | ||
|
||
let (view, doc) = current!(editor); | ||
doc.apply(&transaction, view.id); | ||
doc.append_changes_to_history(view); | ||
view.ensure_cursor_in_view(doc, scrolloff); | ||
|
@@ -6608,6 +6649,38 @@ fn replay_macro(cx: &mut Context) { | |
})); | ||
} | ||
|
||
fn paste_before_joined_with_newline(cx: &mut Context) { | ||
if let Err(err) = paste_joined_impl( | ||
cx.editor, | ||
cx.count(), | ||
PasteJoined::Before, | ||
cx.register, | ||
None, | ||
) { | ||
cx.editor.set_error(err.to_string()); | ||
}; | ||
} | ||
|
||
fn paste_after_joined_with_newline(cx: &mut Context) { | ||
if let Err(err) = | ||
paste_joined_impl(cx.editor, cx.count(), PasteJoined::After, cx.register, None) | ||
{ | ||
cx.editor.set_error(err.to_string()); | ||
}; | ||
} | ||
|
||
fn replace_joined_with_newline(cx: &mut Context) { | ||
if let Err(err) = paste_joined_impl( | ||
cx.editor, | ||
cx.count(), | ||
PasteJoined::Replace, | ||
cx.register, | ||
None, | ||
) { | ||
cx.editor.set_error(err.to_string()); | ||
}; | ||
} | ||
|
||
fn goto_word(cx: &mut Context) { | ||
jump_to_word(cx, Movement::Move) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why collect to a vec here instead of accepting
impl Iterator<Item=&str>
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The issue is that here:
values
from borrows from theeditor
.We then need to
let (view, doc) = current!(editor);
which mutably borrows fromEditor
to create&mut View
and&mut Document
To call
replace_impl
with something likeimpl Iterator<Item=&str>
, each&str
would need to borrow fromEditor
and theView
/Document
would need to mutably borrowI also tried accepting
impl Iterator<Item = String>
, but it did not work because the lifetime of the iterator is still bound to theeditor
, which causes the issues mentioned above