Skip to content

Commit

Permalink
editor: Add rewrap command (#17909)
Browse files Browse the repository at this point in the history
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 <[email protected]>
Co-authored-by: Bennet <[email protected]>
  • Loading branch information
3 people authored Sep 16, 2024
1 parent e7d18ef commit b54b3d6
Show file tree
Hide file tree
Showing 6 changed files with 399 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/editor/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
similar.workspace = true
smallvec.workspace = true
smol.workspace = true
snippet.workspace = true
Expand Down
1 change: 1 addition & 0 deletions crates/editor/src/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ gpui::actions!(
Redo,
RedoSelection,
Rename,
Rewrap,
RestartLanguageServer,
RevealInFileManager,
ReverseLines,
Expand Down
156 changes: 156 additions & 0 deletions crates/editor/src/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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(&current_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(&current_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);
Expand Down
Loading

0 comments on commit b54b3d6

Please sign in to comment.