editor: Add rewrap command (#17909)
This PR adds a rewrap command to the editor. Executing this command will rewrap the text in the selection to the maximum line width: https://github.com/user-attachments/assets/69aa9428-17fd-4315-89f1-f354d0f5f459 If there isn't an active selection, the selection will be expanded to contiguous text from the cursor's location. Rewrapping only works in Markdown, Plain Text, and within comments for any other language. Release Notes: - Added an `editor: rewrap` command for rewrapping text to the maximum line width. --------- Co-authored-by: Antonio Scandurra <me@as-cii.com> Co-authored-by: Bennet <bennet@zed.dev>
This commit is contained in:
co-authored by
Antonio Scandurra
Bennet
parent
e7d18ef359
commit
b54b3d6246
@@ -98,6 +98,7 @@ use language::{
|
||||
};
|
||||
use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange};
|
||||
use linked_editing_ranges::refresh_linked_ranges;
|
||||
use similar::{ChangeTag, TextDiff};
|
||||
use task::{ResolvedTask, TaskTemplate, TaskVariables};
|
||||
|
||||
use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight};
|
||||
@@ -6659,6 +6660,161 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
pub fn rewrap(&mut self, _: &Rewrap, cx: &mut ViewContext<Self>) {
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
let selections = self.selections.all::<Point>(cx);
|
||||
let mut selections = selections.iter().peekable();
|
||||
|
||||
let mut edits = Vec::new();
|
||||
let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
|
||||
|
||||
while let Some(selection) = selections.next() {
|
||||
let mut start_row = selection.start.row;
|
||||
let mut end_row = selection.end.row;
|
||||
|
||||
// Skip selections that overlap with a range that has already been rewrapped.
|
||||
let selection_range = start_row..end_row;
|
||||
if rewrapped_row_ranges
|
||||
.iter()
|
||||
.any(|range| range.overlaps(&selection_range))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut should_rewrap = false;
|
||||
|
||||
if let Some(language_scope) = buffer.language_scope_at(selection.head()) {
|
||||
match language_scope.language_name().0.as_ref() {
|
||||
"Markdown" | "Plain Text" => {
|
||||
should_rewrap = true;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let row = selection.head().row;
|
||||
let indent_size = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let indent_end = Point::new(row, indent_size.len);
|
||||
|
||||
let mut line_prefix = indent_size.chars().collect::<String>();
|
||||
|
||||
if selection.is_empty() {
|
||||
if let Some(comment_prefix) =
|
||||
buffer
|
||||
.language_scope_at(selection.head())
|
||||
.and_then(|language| {
|
||||
language
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.cloned()
|
||||
})
|
||||
{
|
||||
line_prefix.push_str(&comment_prefix);
|
||||
should_rewrap = true;
|
||||
}
|
||||
|
||||
'expand_upwards: while start_row > 0 {
|
||||
let prev_row = start_row - 1;
|
||||
if buffer.contains_str_at(Point::new(prev_row, 0), &line_prefix)
|
||||
&& buffer.line_len(MultiBufferRow(prev_row)) as usize > line_prefix.len()
|
||||
{
|
||||
start_row = prev_row;
|
||||
} else {
|
||||
break 'expand_upwards;
|
||||
}
|
||||
}
|
||||
|
||||
'expand_downwards: while end_row < buffer.max_point().row {
|
||||
let next_row = end_row + 1;
|
||||
if buffer.contains_str_at(Point::new(next_row, 0), &line_prefix)
|
||||
&& buffer.line_len(MultiBufferRow(next_row)) as usize > line_prefix.len()
|
||||
{
|
||||
end_row = next_row;
|
||||
} else {
|
||||
break 'expand_downwards;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !should_rewrap {
|
||||
continue;
|
||||
}
|
||||
|
||||
let start = Point::new(start_row, 0);
|
||||
let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
|
||||
let selection_text = buffer.text_for_range(start..end).collect::<String>();
|
||||
let unwrapped_text = selection_text
|
||||
.lines()
|
||||
.map(|line| line.strip_prefix(&line_prefix).unwrap())
|
||||
.join(" ");
|
||||
let wrap_column = buffer
|
||||
.settings_at(Point::new(start_row, 0), cx)
|
||||
.preferred_line_length as usize;
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
for word in unwrapped_text.split_whitespace() {
|
||||
if current_line.len() + word.len() >= wrap_column {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
}
|
||||
|
||||
if current_line.len() > line_prefix.len() {
|
||||
current_line.push(' ');
|
||||
}
|
||||
|
||||
current_line.push_str(word);
|
||||
}
|
||||
|
||||
if !current_line.is_empty() {
|
||||
wrapped_text.push_str(¤t_line);
|
||||
}
|
||||
|
||||
let diff = TextDiff::from_lines(&selection_text, &wrapped_text);
|
||||
let mut offset = start.to_offset(&buffer);
|
||||
let mut moved_since_edit = true;
|
||||
|
||||
for change in diff.iter_all_changes() {
|
||||
let value = change.value();
|
||||
match change.tag() {
|
||||
ChangeTag::Equal => {
|
||||
offset += value.len();
|
||||
moved_since_edit = true;
|
||||
}
|
||||
ChangeTag::Delete => {
|
||||
let start = buffer.anchor_after(offset);
|
||||
let end = buffer.anchor_before(offset + value.len());
|
||||
|
||||
if moved_since_edit {
|
||||
edits.push((start..end, String::new()));
|
||||
} else {
|
||||
edits.last_mut().unwrap().0.end = end;
|
||||
}
|
||||
|
||||
offset += value.len();
|
||||
moved_since_edit = false;
|
||||
}
|
||||
ChangeTag::Insert => {
|
||||
if moved_since_edit {
|
||||
let anchor = buffer.anchor_after(offset);
|
||||
edits.push((anchor..anchor, value.to_string()));
|
||||
} else {
|
||||
edits.last_mut().unwrap().1.push_str(value);
|
||||
}
|
||||
|
||||
moved_since_edit = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rewrapped_row_ranges.push(start_row..=end_row);
|
||||
}
|
||||
|
||||
self.buffer
|
||||
.update(cx, |buffer, cx| buffer.edit(edits, None, cx));
|
||||
}
|
||||
|
||||
pub fn cut(&mut self, _: &Cut, cx: &mut ViewContext<Self>) {
|
||||
let mut text = String::new();
|
||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||
|
||||
Reference in New Issue
Block a user