Detailed changes
@@ -0,0 +1,352 @@
+use super::*;
+
+impl Editor {
+ pub fn style(&mut self, cx: &App) -> &EditorStyle {
+ match self.style {
+ Some(ref style) => style,
+ None => {
+ let style = self.create_style(cx);
+ self.style.insert(style)
+ }
+ }
+ }
+
+ pub fn set_soft_wrap_mode(
+ &mut self,
+ mode: language_settings::SoftWrap,
+ cx: &mut Context<Self>,
+ ) {
+ self.soft_wrap_mode_override = Some(mode);
+ cx.notify();
+ }
+
+ pub fn set_hard_wrap(&mut self, hard_wrap: Option<usize>, cx: &mut Context<Self>) {
+ self.hard_wrap = hard_wrap;
+ cx.notify();
+ }
+
+ pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) {
+ self.text_style_refinement = Some(style);
+ }
+
+ /// called by the Element so we know what style we were most recently rendered with.
+ pub fn set_style(&mut self, style: EditorStyle, window: &mut Window, cx: &mut Context<Self>) {
+ // We intentionally do not inform the display map about the minimap style
+ // so that wrapping is not recalculated and stays consistent for the editor
+ // and its linked minimap.
+ if !self.mode.is_minimap() {
+ let font = style.text.font();
+ let font_size = style.text.font_size.to_pixels(window.rem_size());
+ let display_map = self
+ .placeholder_display_map
+ .as_ref()
+ .filter(|_| self.is_empty(cx))
+ .unwrap_or(&self.display_map);
+
+ display_map.update(cx, |map, cx| map.set_font(font, font_size, cx));
+ }
+ self.style = Some(style);
+ }
+
+ pub fn set_soft_wrap(&mut self) {
+ self.soft_wrap_mode_override = Some(language_settings::SoftWrap::EditorWidth)
+ }
+
+ pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context<Self>) {
+ self.show_wrap_guides = Some(show_wrap_guides);
+ cx.notify();
+ }
+
+ pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context<Self>) {
+ self.show_indent_guides = Some(show_indent_guides);
+ cx.notify();
+ }
+
+ pub fn disable_indent_guides_for_buffer(
+ &mut self,
+ buffer_id: BufferId,
+ cx: &mut Context<Self>,
+ ) {
+ self.buffers_with_disabled_indent_guides.insert(buffer_id);
+ cx.notify();
+ }
+
+ pub fn toggle_line_numbers(
+ &mut self,
+ _: &ToggleLineNumbers,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let mut editor_settings = EditorSettings::get_global(cx).clone();
+ editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers;
+ EditorSettings::override_global(editor_settings, cx);
+ }
+
+ pub fn line_numbers_enabled(&self, cx: &App) -> bool {
+ if let Some(show_line_numbers) = self.show_line_numbers {
+ return show_line_numbers;
+ }
+ EditorSettings::get_global(cx).gutter.line_numbers
+ }
+
+ pub fn relative_line_numbers(&self, cx: &App) -> RelativeLineNumbers {
+ match (
+ self.use_relative_line_numbers,
+ EditorSettings::get_global(cx).relative_line_numbers,
+ ) {
+ (None, setting) => setting,
+ (Some(false), _) => RelativeLineNumbers::Disabled,
+ (Some(true), RelativeLineNumbers::Wrapped) => RelativeLineNumbers::Wrapped,
+ (Some(true), _) => RelativeLineNumbers::Enabled,
+ }
+ }
+
+ pub fn set_relative_line_number(&mut self, is_relative: Option<bool>, cx: &mut Context<Self>) {
+ self.use_relative_line_numbers = is_relative;
+ cx.notify();
+ }
+
+ pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context<Self>) {
+ self.show_gutter = show_gutter;
+ cx.notify();
+ }
+
+ pub fn set_show_vertical_scrollbar(&mut self, show: bool, cx: &mut Context<Self>) {
+ self.show_scrollbars.vertical = show;
+ cx.notify();
+ }
+
+ pub fn set_show_horizontal_scrollbar(&mut self, show: bool, cx: &mut Context<Self>) {
+ self.show_scrollbars.horizontal = show;
+ cx.notify();
+ }
+
+ pub fn set_minimap_visibility(
+ &mut self,
+ minimap_visibility: MinimapVisibility,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.minimap_visibility != minimap_visibility {
+ if minimap_visibility.visible() && self.minimap.is_none() {
+ let minimap_settings = EditorSettings::get_global(cx).minimap;
+ self.minimap =
+ self.create_minimap(minimap_settings.with_show_override(), window, cx);
+ }
+ self.minimap_visibility = minimap_visibility;
+ cx.notify();
+ }
+ }
+
+ pub fn disable_scrollbars_and_minimap(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.set_show_scrollbars(false, cx);
+ self.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
+ }
+
+ pub fn hide_minimap_by_default(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.set_minimap_visibility(self.minimap_visibility.hidden(), window, cx);
+ }
+
+ /// Normally the text in full mode and auto height editors is padded on the
+ /// left side by roughly half a character width for improved hit testing.
+ ///
+ /// Use this method to disable this for cases where this is not wanted (e.g.
+ /// if you want to align the editor text with some other text above or below)
+ /// or if you want to add this padding to single-line editors.
+ pub fn set_offset_content(&mut self, offset_content: bool, cx: &mut Context<Self>) {
+ self.offset_content = offset_content;
+ cx.notify();
+ }
+
+ pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
+ self.show_line_numbers = Some(show_line_numbers);
+ cx.notify();
+ }
+
+ pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context<Self>) {
+ self.disable_expand_excerpt_buttons = true;
+ cx.notify();
+ }
+
+ pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
+ self.show_git_diff_gutter = Some(show_git_diff_gutter);
+ cx.notify();
+ }
+
+ pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context<Self>) {
+ self.show_code_actions = Some(show_code_actions);
+ cx.notify();
+ }
+
+ pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context<Self>) {
+ self.show_runnables = Some(show_runnables);
+ cx.notify();
+ }
+
+ pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context<Self>) {
+ self.show_breakpoints = Some(show_breakpoints);
+ cx.notify();
+ }
+
+ pub fn set_show_diff_review_button(&mut self, show: bool, cx: &mut Context<Self>) {
+ self.show_diff_review_button = show;
+ cx.notify();
+ }
+
+ fn set_show_scrollbars(&mut self, show: bool, cx: &mut Context<Self>) {
+ self.show_scrollbars = ScrollbarAxes {
+ horizontal: show,
+ vertical: show,
+ };
+ cx.notify();
+ }
+
+ pub(super) fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> {
+ let mut wrap_guides = smallvec![];
+
+ if self.show_wrap_guides == Some(false) {
+ return wrap_guides;
+ }
+
+ let settings = self.buffer.read(cx).language_settings(cx);
+ if settings.show_wrap_guides {
+ match self.soft_wrap_mode(cx) {
+ SoftWrap::Bounded(soft_wrap) => {
+ wrap_guides.push((soft_wrap as usize, true));
+ }
+ SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {}
+ }
+ wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
+ }
+
+ wrap_guides
+ }
+
+ pub(super) fn soft_wrap_mode(&self, cx: &App) -> SoftWrap {
+ let settings = self.buffer.read(cx).language_settings(cx);
+ let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap);
+ match mode {
+ language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => {
+ SoftWrap::None
+ }
+ language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
+ language_settings::SoftWrap::Bounded => {
+ SoftWrap::Bounded(settings.preferred_line_length)
+ }
+ }
+ }
+
+ // Called by the element. This method is not designed to be called outside of the editor
+ // element's layout code because it does not notify when rewrapping is computed synchronously.
+ pub(super) fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut App) -> bool {
+ if self.is_empty(cx) {
+ self.placeholder_display_map
+ .as_ref()
+ .map_or(false, |display_map| {
+ display_map.update(cx, |map, cx| map.set_wrap_width(width, cx))
+ })
+ } else {
+ self.display_map
+ .update(cx, |map, cx| map.set_wrap_width(width, cx))
+ }
+ }
+
+ pub(super) fn toggle_soft_wrap(
+ &mut self,
+ _: &ToggleSoftWrap,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if self.soft_wrap_mode_override.is_some() {
+ self.soft_wrap_mode_override.take();
+ } else {
+ let soft_wrap = match self.soft_wrap_mode(cx) {
+ SoftWrap::GitDiff => return,
+ SoftWrap::None => language_settings::SoftWrap::EditorWidth,
+ SoftWrap::EditorWidth | SoftWrap::Bounded(_) => language_settings::SoftWrap::None,
+ };
+ self.soft_wrap_mode_override = Some(soft_wrap);
+ }
+ cx.notify();
+ }
+
+ pub(super) fn toggle_tab_bar(
+ &mut self,
+ _: &ToggleTabBar,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let Some(workspace) = self.workspace() else {
+ return;
+ };
+ let fs = workspace.read(cx).app_state().fs.clone();
+ let current_show = TabBarSettings::get_global(cx).show;
+ update_settings_file(fs, cx, move |setting, _| {
+ setting.tab_bar.get_or_insert_default().show = Some(!current_show);
+ });
+ }
+
+ pub(super) fn toggle_indent_guides(
+ &mut self,
+ _: &ToggleIndentGuides,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
+ self.buffer
+ .read(cx)
+ .language_settings(cx)
+ .indent_guides
+ .enabled
+ });
+ self.show_indent_guides = Some(!currently_enabled);
+ cx.notify();
+ }
+
+ pub(super) fn should_show_indent_guides(&self) -> Option<bool> {
+ self.show_indent_guides
+ }
+
+ pub(super) fn has_indent_guides_disabled_for_buffer(&self, buffer_id: BufferId) -> bool {
+ self.buffers_with_disabled_indent_guides
+ .contains(&buffer_id)
+ }
+
+ pub(super) fn toggle_relative_line_numbers(
+ &mut self,
+ _: &ToggleRelativeLineNumbers,
+ _: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let is_relative = self.relative_line_numbers(cx);
+ self.set_relative_line_number(Some(!is_relative.enabled()), cx)
+ }
+
+ pub(super) fn set_number_deleted_lines(&mut self, number: bool, cx: &mut Context<Self>) {
+ self.number_deleted_lines = number;
+ cx.notify();
+ }
+
+ pub fn set_delegate_open_excerpts(&mut self, delegate: bool) {
+ self.delegate_open_excerpts = delegate;
+ }
+
+ pub(super) fn set_delegate_expand_excerpts(&mut self, delegate: bool) {
+ self.delegate_expand_excerpts = delegate;
+ }
+
+ pub(super) fn set_delegate_stage_and_restore(&mut self, delegate: bool) {
+ self.delegate_stage_and_restore = delegate;
+ }
+
+ pub(super) fn set_on_local_selections_changed(
+ &mut self,
+ callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
+ ) {
+ self.on_local_selections_changed = callback;
+ }
+
+ pub(super) fn set_suppress_selection_callback(&mut self, suppress: bool) {
+ self.suppress_selection_callback = suppress;
+ }
+}
@@ -57,8 +57,9 @@ mod signature_help;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
-#[path = "editor/diagnostics.rs"]
+mod config;
mod diagnostics;
+mod rewrap;
pub(crate) use actions::*;
use diagnostics::{ActiveDiagnostic, GlobalDiagnosticRenderer, InlineDiagnostic};
@@ -5166,7 +5167,7 @@ impl Editor {
.line_len(MultiBufferRow(latest.start.row))
== latest.start.column
{
- this.rewrap_impl(
+ this.rewrap(
RewrapOptions {
override_language_settings: true,
preserve_existing_whitespace: true,
@@ -13856,410 +13857,6 @@ impl Editor {
});
}
- pub fn rewrap(&mut self, _: &Rewrap, _: &mut Window, cx: &mut Context<Self>) {
- if self.read_only(cx) {
- return;
- }
- if self.mode.is_single_line() {
- cx.propagate();
- return;
- }
-
- self.rewrap_impl(RewrapOptions::default(), cx)
- }
-
- pub fn rewrap_impl(&mut self, options: RewrapOptions, cx: &mut Context<Self>) {
- if self.read_only(cx) {
- return;
- }
- let buffer = self.buffer.read(cx).snapshot(cx);
- let selections = self.selections.all::<Point>(&self.display_snapshot(cx));
-
- #[derive(Clone, Debug, PartialEq)]
- enum CommentFormat {
- /// single line comment, with prefix for line
- Line(String),
- /// single line within a block comment, with prefix for line
- BlockLine(String),
- /// a single line of a block comment that includes the initial delimiter
- BlockCommentWithStart(BlockCommentConfig),
- /// a single line of a block comment that includes the ending delimiter
- BlockCommentWithEnd(BlockCommentConfig),
- }
-
- // Split selections to respect paragraph, indent, and comment prefix boundaries.
- let wrap_ranges = selections.into_iter().flat_map(|selection| {
- let language_settings = buffer.language_settings_at(selection.head(), cx);
- let language_scope = buffer.language_scope_at(selection.head());
-
- let indent_and_prefix_for_row =
- |row: u32| -> (IndentSize, Option<CommentFormat>, Option<String>) {
- let indent = buffer.indent_size_for_line(MultiBufferRow(row));
- let (comment_prefix, rewrap_prefix) = if let Some(language_scope) =
- &language_scope
- {
- let indent_end = Point::new(row, indent.len);
- let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
- let line_text_after_indent = buffer
- .text_for_range(indent_end..line_end)
- .collect::<String>();
-
- let is_within_comment_override = buffer
- .language_scope_at(indent_end)
- .is_some_and(|scope| scope.override_name() == Some("comment"));
- let comment_delimiters = if is_within_comment_override {
- // we are within a comment syntax node, but we don't
- // yet know what kind of comment: block, doc or line
- match (
- language_scope.documentation_comment(),
- language_scope.block_comment(),
- ) {
- (Some(config), _) | (_, Some(config))
- if buffer.contains_str_at(indent_end, &config.start) =>
- {
- Some(CommentFormat::BlockCommentWithStart(config.clone()))
- }
- (Some(config), _) | (_, Some(config))
- if line_text_after_indent.ends_with(config.end.as_ref()) =>
- {
- Some(CommentFormat::BlockCommentWithEnd(config.clone()))
- }
- (Some(config), _) | (_, Some(config))
- if !config.prefix.is_empty()
- && buffer.contains_str_at(indent_end, &config.prefix) =>
- {
- Some(CommentFormat::BlockLine(config.prefix.to_string()))
- }
- (_, _) => language_scope
- .line_comment_prefixes()
- .iter()
- .find(|prefix| buffer.contains_str_at(indent_end, prefix))
- .map(|prefix| CommentFormat::Line(prefix.to_string())),
- }
- } else {
- // we not in an overridden comment node, but we may
- // be within a non-overridden line comment node
- language_scope
- .line_comment_prefixes()
- .iter()
- .find(|prefix| buffer.contains_str_at(indent_end, prefix))
- .map(|prefix| CommentFormat::Line(prefix.to_string()))
- };
-
- let rewrap_prefix = language_scope
- .rewrap_prefixes()
- .iter()
- .find_map(|prefix_regex| {
- prefix_regex.find(&line_text_after_indent).map(|mat| {
- if mat.start() == 0 {
- Some(mat.as_str().to_string())
- } else {
- None
- }
- })
- })
- .flatten();
- (comment_delimiters, rewrap_prefix)
- } else {
- (None, None)
- };
- (indent, comment_prefix, rewrap_prefix)
- };
-
- let mut start_row = selection.start.row;
- let mut end_row = selection.end.row;
-
- if selection.is_empty() {
- let cursor_row = selection.start.row;
-
- let (mut indent_size, comment_prefix, _) = indent_and_prefix_for_row(cursor_row);
- let line_prefix = match &comment_prefix {
- Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
- Some(prefix.as_str())
- }
- Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
- prefix, ..
- })) => Some(prefix.as_ref()),
- Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
- start: _,
- end: _,
- prefix,
- tab_size,
- })) => {
- indent_size.len += tab_size;
- Some(prefix.as_ref())
- }
- None => None,
- };
- let indent_prefix = indent_size.chars().collect::<String>();
- let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
-
- '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()
- && !buffer.is_line_blank(MultiBufferRow(prev_row))
- {
- 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()
- && !buffer.is_line_blank(MultiBufferRow(next_row))
- {
- end_row = next_row;
- } else {
- break 'expand_downwards;
- }
- }
- }
-
- let mut non_blank_rows_iter = (start_row..=end_row)
- .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
- .peekable();
-
- let first_row = if let Some(&row) = non_blank_rows_iter.peek() {
- row
- } else {
- return Vec::new();
- };
-
- let mut ranges = Vec::new();
-
- let mut current_range_start = first_row;
- let mut prev_row = first_row;
- let (
- mut current_range_indent,
- mut current_range_comment_delimiters,
- mut current_range_rewrap_prefix,
- ) = indent_and_prefix_for_row(first_row);
-
- for row in non_blank_rows_iter.skip(1) {
- let has_paragraph_break = row > prev_row + 1;
-
- let (row_indent, row_comment_delimiters, row_rewrap_prefix) =
- indent_and_prefix_for_row(row);
-
- let has_indent_change = row_indent != current_range_indent;
- let has_comment_change = row_comment_delimiters != current_range_comment_delimiters;
-
- let has_boundary_change = has_comment_change
- || row_rewrap_prefix.is_some()
- || (has_indent_change && current_range_comment_delimiters.is_some());
-
- if has_paragraph_break || has_boundary_change {
- ranges.push((
- language_settings.clone(),
- Point::new(current_range_start, 0)
- ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
- current_range_indent,
- current_range_comment_delimiters.clone(),
- current_range_rewrap_prefix.clone(),
- ));
- current_range_start = row;
- current_range_indent = row_indent;
- current_range_comment_delimiters = row_comment_delimiters;
- current_range_rewrap_prefix = row_rewrap_prefix;
- }
- prev_row = row;
- }
-
- ranges.push((
- language_settings.clone(),
- Point::new(current_range_start, 0)
- ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
- current_range_indent,
- current_range_comment_delimiters,
- current_range_rewrap_prefix,
- ));
-
- ranges
- });
-
- let mut edits = Vec::new();
- let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
-
- for (language_settings, wrap_range, mut indent_size, comment_prefix, rewrap_prefix) in
- wrap_ranges
- {
- let start_row = wrap_range.start.row;
- let end_row = wrap_range.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 tab_size = language_settings.tab_size;
-
- let (line_prefix, inside_comment) = match &comment_prefix {
- Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
- (Some(prefix.as_str()), true)
- }
- Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig { prefix, .. })) => {
- (Some(prefix.as_ref()), true)
- }
- Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
- start: _,
- end: _,
- prefix,
- tab_size,
- })) => {
- indent_size.len += tab_size;
- (Some(prefix.as_ref()), true)
- }
- None => (None, false),
- };
- let indent_prefix = indent_size.chars().collect::<String>();
- let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
-
- let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
- RewrapBehavior::InComments => inside_comment,
- RewrapBehavior::InSelections => !wrap_range.is_empty(),
- RewrapBehavior::Anywhere => true,
- };
-
- let should_rewrap = options.override_language_settings
- || allow_rewrap_based_on_language
- || self.hard_wrap.is_some();
- if !should_rewrap {
- continue;
- }
-
- let start = Point::new(start_row, 0);
- let start_offset = ToOffset::to_offset(&start, &buffer);
- let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
- let selection_text = buffer.text_for_range(start..end).collect::<String>();
- let mut first_line_delimiter = None;
- let mut last_line_delimiter = None;
- let Some(lines_without_prefixes) = selection_text
- .lines()
- .enumerate()
- .map(|(ix, line)| {
- let line_trimmed = line.trim_start();
- if rewrap_prefix.is_some() && ix > 0 {
- Ok(line_trimmed)
- } else if let Some(
- CommentFormat::BlockCommentWithStart(BlockCommentConfig {
- start,
- prefix,
- end,
- tab_size,
- })
- | CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
- start,
- prefix,
- end,
- tab_size,
- }),
- ) = &comment_prefix
- {
- let line_trimmed = line_trimmed
- .strip_prefix(start.as_ref())
- .map(|s| {
- let mut indent_size = indent_size;
- indent_size.len -= tab_size;
- let indent_prefix: String = indent_size.chars().collect();
- first_line_delimiter = Some((indent_prefix, start));
- s.trim_start()
- })
- .unwrap_or(line_trimmed);
- let line_trimmed = line_trimmed
- .strip_suffix(end.as_ref())
- .map(|s| {
- last_line_delimiter = Some(end);
- s.trim_end()
- })
- .unwrap_or(line_trimmed);
- let line_trimmed = line_trimmed
- .strip_prefix(prefix.as_ref())
- .unwrap_or(line_trimmed);
- Ok(line_trimmed)
- } else if let Some(CommentFormat::BlockLine(prefix)) = &comment_prefix {
- line_trimmed.strip_prefix(prefix).with_context(|| {
- format!("line did not start with prefix {prefix:?}: {line:?}")
- })
- } else {
- line_trimmed
- .strip_prefix(&line_prefix.trim_start())
- .with_context(|| {
- format!("line did not start with prefix {line_prefix:?}: {line:?}")
- })
- }
- })
- .collect::<Result<Vec<_>, _>>()
- .log_err()
- else {
- continue;
- };
-
- let wrap_column = options.line_length.or(self.hard_wrap).unwrap_or_else(|| {
- buffer
- .language_settings_at(Point::new(start_row, 0), cx)
- .preferred_line_length as usize
- });
-
- let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
- format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
- } else {
- line_prefix.clone()
- };
-
- let wrapped_text = {
- let mut wrapped_text = wrap_with_prefix(
- line_prefix,
- subsequent_lines_prefix,
- lines_without_prefixes.join("\n"),
- wrap_column,
- tab_size,
- options.preserve_existing_whitespace,
- );
-
- if let Some((indent, delimiter)) = first_line_delimiter {
- wrapped_text = format!("{indent}{delimiter}\n{wrapped_text}");
- }
- if let Some(last_line) = last_line_delimiter {
- wrapped_text = format!("{wrapped_text}\n{indent_prefix}{last_line}");
- }
-
- wrapped_text
- };
-
- // TODO: should always use char-based diff while still supporting cursor behavior that
- // matches vim.
- let mut diff_options = DiffOptions::default();
- if options.override_language_settings {
- diff_options.max_word_diff_len = 0;
- diff_options.max_word_diff_line_count = 0;
- } else {
- diff_options.max_word_diff_len = usize::MAX;
- diff_options.max_word_diff_line_count = usize::MAX;
- }
-
- for (old_range, new_text) in
- text_diff_with_options(&selection_text, &wrapped_text, diff_options)
- {
- let edit_start = buffer.anchor_after(start_offset + old_range.start);
- let edit_end = buffer.anchor_after(start_offset + old_range.end);
- edits.push((edit_start..edit_end, new_text));
- }
-
- rewrapped_row_ranges.push(start_row..=end_row);
- }
-
- self.buffer
- .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
- }
-
pub fn cut_common(
&mut self,
cut_no_selection_line: bool,
@@ -21579,332 +21176,6 @@ impl Editor {
.filter(|_| self.minimap_visibility.visible())
}
- pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> {
- let mut wrap_guides = smallvec![];
-
- if self.show_wrap_guides == Some(false) {
- return wrap_guides;
- }
-
- let settings = self.buffer.read(cx).language_settings(cx);
- if settings.show_wrap_guides {
- match self.soft_wrap_mode(cx) {
- SoftWrap::Bounded(soft_wrap) => {
- wrap_guides.push((soft_wrap as usize, true));
- }
- SoftWrap::GitDiff | SoftWrap::None | SoftWrap::EditorWidth => {}
- }
- wrap_guides.extend(settings.wrap_guides.iter().map(|guide| (*guide, false)))
- }
-
- wrap_guides
- }
-
- pub fn soft_wrap_mode(&self, cx: &App) -> SoftWrap {
- let settings = self.buffer.read(cx).language_settings(cx);
- let mode = self.soft_wrap_mode_override.unwrap_or(settings.soft_wrap);
- match mode {
- language_settings::SoftWrap::PreferLine | language_settings::SoftWrap::None => {
- SoftWrap::None
- }
- language_settings::SoftWrap::EditorWidth => SoftWrap::EditorWidth,
- language_settings::SoftWrap::Bounded => {
- SoftWrap::Bounded(settings.preferred_line_length)
- }
- }
- }
-
- pub fn set_soft_wrap_mode(
- &mut self,
- mode: language_settings::SoftWrap,
- cx: &mut Context<Self>,
- ) {
- self.soft_wrap_mode_override = Some(mode);
- cx.notify();
- }
-
- pub fn set_hard_wrap(&mut self, hard_wrap: Option<usize>, cx: &mut Context<Self>) {
- self.hard_wrap = hard_wrap;
- cx.notify();
- }
-
- pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) {
- self.text_style_refinement = Some(style);
- }
-
- /// called by the Element so we know what style we were most recently rendered with.
- pub fn set_style(&mut self, style: EditorStyle, window: &mut Window, cx: &mut Context<Self>) {
- // We intentionally do not inform the display map about the minimap style
- // so that wrapping is not recalculated and stays consistent for the editor
- // and its linked minimap.
- if !self.mode.is_minimap() {
- let font = style.text.font();
- let font_size = style.text.font_size.to_pixels(window.rem_size());
- let display_map = self
- .placeholder_display_map
- .as_ref()
- .filter(|_| self.is_empty(cx))
- .unwrap_or(&self.display_map);
-
- display_map.update(cx, |map, cx| map.set_font(font, font_size, cx));
- }
- self.style = Some(style);
- }
-
- pub fn style(&mut self, cx: &App) -> &EditorStyle {
- if self.style.is_none() {
- self.style = Some(self.create_style(cx));
- }
- self.style.as_ref().unwrap()
- }
-
- // Called by the element. This method is not designed to be called outside of the editor
- // element's layout code because it does not notify when rewrapping is computed synchronously.
- pub(crate) fn set_wrap_width(&self, width: Option<Pixels>, cx: &mut App) -> bool {
- if self.is_empty(cx) {
- self.placeholder_display_map
- .as_ref()
- .map_or(false, |display_map| {
- display_map.update(cx, |map, cx| map.set_wrap_width(width, cx))
- })
- } else {
- self.display_map
- .update(cx, |map, cx| map.set_wrap_width(width, cx))
- }
- }
-
- pub fn set_soft_wrap(&mut self) {
- self.soft_wrap_mode_override = Some(language_settings::SoftWrap::EditorWidth)
- }
-
- pub fn toggle_soft_wrap(&mut self, _: &ToggleSoftWrap, _: &mut Window, cx: &mut Context<Self>) {
- if self.soft_wrap_mode_override.is_some() {
- self.soft_wrap_mode_override.take();
- } else {
- let soft_wrap = match self.soft_wrap_mode(cx) {
- SoftWrap::GitDiff => return,
- SoftWrap::None => language_settings::SoftWrap::EditorWidth,
- SoftWrap::EditorWidth | SoftWrap::Bounded(_) => language_settings::SoftWrap::None,
- };
- self.soft_wrap_mode_override = Some(soft_wrap);
- }
- cx.notify();
- }
-
- pub fn toggle_tab_bar(&mut self, _: &ToggleTabBar, _: &mut Window, cx: &mut Context<Self>) {
- let Some(workspace) = self.workspace() else {
- return;
- };
- let fs = workspace.read(cx).app_state().fs.clone();
- let current_show = TabBarSettings::get_global(cx).show;
- update_settings_file(fs, cx, move |setting, _| {
- setting.tab_bar.get_or_insert_default().show = Some(!current_show);
- });
- }
-
- pub fn toggle_indent_guides(
- &mut self,
- _: &ToggleIndentGuides,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let currently_enabled = self.should_show_indent_guides().unwrap_or_else(|| {
- self.buffer
- .read(cx)
- .language_settings(cx)
- .indent_guides
- .enabled
- });
- self.show_indent_guides = Some(!currently_enabled);
- cx.notify();
- }
-
- fn should_show_indent_guides(&self) -> Option<bool> {
- self.show_indent_guides
- }
-
- pub fn disable_indent_guides_for_buffer(
- &mut self,
- buffer_id: BufferId,
- cx: &mut Context<Self>,
- ) {
- self.buffers_with_disabled_indent_guides.insert(buffer_id);
- cx.notify();
- }
-
- pub fn has_indent_guides_disabled_for_buffer(&self, buffer_id: BufferId) -> bool {
- self.buffers_with_disabled_indent_guides
- .contains(&buffer_id)
- }
-
- pub fn toggle_line_numbers(
- &mut self,
- _: &ToggleLineNumbers,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let mut editor_settings = EditorSettings::get_global(cx).clone();
- editor_settings.gutter.line_numbers = !editor_settings.gutter.line_numbers;
- EditorSettings::override_global(editor_settings, cx);
- }
-
- pub fn line_numbers_enabled(&self, cx: &App) -> bool {
- if let Some(show_line_numbers) = self.show_line_numbers {
- return show_line_numbers;
- }
- EditorSettings::get_global(cx).gutter.line_numbers
- }
-
- pub fn relative_line_numbers(&self, cx: &App) -> RelativeLineNumbers {
- match (
- self.use_relative_line_numbers,
- EditorSettings::get_global(cx).relative_line_numbers,
- ) {
- (None, setting) => setting,
- (Some(false), _) => RelativeLineNumbers::Disabled,
- (Some(true), RelativeLineNumbers::Wrapped) => RelativeLineNumbers::Wrapped,
- (Some(true), _) => RelativeLineNumbers::Enabled,
- }
- }
-
- pub fn toggle_relative_line_numbers(
- &mut self,
- _: &ToggleRelativeLineNumbers,
- _: &mut Window,
- cx: &mut Context<Self>,
- ) {
- let is_relative = self.relative_line_numbers(cx);
- self.set_relative_line_number(Some(!is_relative.enabled()), cx)
- }
-
- pub fn set_relative_line_number(&mut self, is_relative: Option<bool>, cx: &mut Context<Self>) {
- self.use_relative_line_numbers = is_relative;
- cx.notify();
- }
-
- pub fn set_show_gutter(&mut self, show_gutter: bool, cx: &mut Context<Self>) {
- self.show_gutter = show_gutter;
- cx.notify();
- }
-
- pub fn set_show_scrollbars(&mut self, show: bool, cx: &mut Context<Self>) {
- self.show_scrollbars = ScrollbarAxes {
- horizontal: show,
- vertical: show,
- };
- cx.notify();
- }
-
- pub fn set_show_vertical_scrollbar(&mut self, show: bool, cx: &mut Context<Self>) {
- self.show_scrollbars.vertical = show;
- cx.notify();
- }
-
- pub fn set_show_horizontal_scrollbar(&mut self, show: bool, cx: &mut Context<Self>) {
- self.show_scrollbars.horizontal = show;
- cx.notify();
- }
-
- pub fn set_minimap_visibility(
- &mut self,
- minimap_visibility: MinimapVisibility,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) {
- if self.minimap_visibility != minimap_visibility {
- if minimap_visibility.visible() && self.minimap.is_none() {
- let minimap_settings = EditorSettings::get_global(cx).minimap;
- self.minimap =
- self.create_minimap(minimap_settings.with_show_override(), window, cx);
- }
- self.minimap_visibility = minimap_visibility;
- cx.notify();
- }
- }
-
- pub fn disable_scrollbars_and_minimap(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.set_show_scrollbars(false, cx);
- self.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
- }
-
- pub fn hide_minimap_by_default(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- self.set_minimap_visibility(self.minimap_visibility.hidden(), window, cx);
- }
-
- /// Normally the text in full mode and auto height editors is padded on the
- /// left side by roughly half a character width for improved hit testing.
- ///
- /// Use this method to disable this for cases where this is not wanted (e.g.
- /// if you want to align the editor text with some other text above or below)
- /// or if you want to add this padding to single-line editors.
- pub fn set_offset_content(&mut self, offset_content: bool, cx: &mut Context<Self>) {
- self.offset_content = offset_content;
- cx.notify();
- }
-
- pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context<Self>) {
- self.show_line_numbers = Some(show_line_numbers);
- cx.notify();
- }
-
- pub fn disable_expand_excerpt_buttons(&mut self, cx: &mut Context<Self>) {
- self.disable_expand_excerpt_buttons = true;
- cx.notify();
- }
-
- pub fn set_number_deleted_lines(&mut self, number: bool, cx: &mut Context<Self>) {
- self.number_deleted_lines = number;
- cx.notify();
- }
-
- pub fn set_delegate_expand_excerpts(&mut self, delegate: bool) {
- self.delegate_expand_excerpts = delegate;
- }
-
- pub fn set_delegate_stage_and_restore(&mut self, delegate: bool) {
- self.delegate_stage_and_restore = delegate;
- }
-
- pub fn set_delegate_open_excerpts(&mut self, delegate: bool) {
- self.delegate_open_excerpts = delegate;
- }
-
- pub fn set_on_local_selections_changed(
- &mut self,
- callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
- ) {
- self.on_local_selections_changed = callback;
- }
-
- pub fn set_suppress_selection_callback(&mut self, suppress: bool) {
- self.suppress_selection_callback = suppress;
- }
-
- pub fn set_show_git_diff_gutter(&mut self, show_git_diff_gutter: bool, cx: &mut Context<Self>) {
- self.show_git_diff_gutter = Some(show_git_diff_gutter);
- cx.notify();
- }
-
- pub fn set_show_code_actions(&mut self, show_code_actions: bool, cx: &mut Context<Self>) {
- self.show_code_actions = Some(show_code_actions);
- cx.notify();
- }
-
- pub fn set_show_runnables(&mut self, show_runnables: bool, cx: &mut Context<Self>) {
- self.show_runnables = Some(show_runnables);
- cx.notify();
- }
-
- pub fn set_show_breakpoints(&mut self, show_breakpoints: bool, cx: &mut Context<Self>) {
- self.show_breakpoints = Some(show_breakpoints);
- cx.notify();
- }
-
- pub fn set_show_diff_review_button(&mut self, show: bool, cx: &mut Context<Self>) {
- self.show_diff_review_button = show;
- cx.notify();
- }
-
pub fn show_diff_review_button(&self) -> bool {
self.show_diff_review_button
}
@@ -23124,16 +22395,6 @@ impl Editor {
cx.notify()
}
- pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut Context<Self>) {
- self.show_wrap_guides = Some(show_wrap_guides);
- cx.notify();
- }
-
- pub fn set_show_indent_guides(&mut self, show_indent_guides: bool, cx: &mut Context<Self>) {
- self.show_indent_guides = Some(show_indent_guides);
- cx.notify();
- }
-
pub fn working_directory(&self, cx: &App) -> Option<PathBuf> {
if let Some(buffer) = self.buffer().read(cx).as_singleton() {
if let Some(file) = buffer.read(cx).file().and_then(|f| f.as_local())
@@ -26853,393 +26114,6 @@ fn update_uncommitted_diff_for_buffer(
})
}
-fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
- let tab_size = tab_size.get() as usize;
- let mut width = offset;
-
- for ch in text.chars() {
- width += if ch == '\t' {
- tab_size - (width % tab_size)
- } else {
- 1
- };
- }
-
- width - offset
-}
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- fn test_string_size_with_expanded_tabs() {
- let nz = |val| NonZeroU32::new(val).unwrap();
- assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
- assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
- assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
- assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
- assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
- assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
- assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
- assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
- }
-}
-
-/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
-struct WordBreakingTokenizer<'a> {
- input: &'a str,
-}
-
-impl<'a> WordBreakingTokenizer<'a> {
- fn new(input: &'a str) -> Self {
- Self { input }
- }
-}
-
-fn is_char_ideographic(ch: char) -> bool {
- use unicode_script::Script::*;
- use unicode_script::UnicodeScript;
- matches!(ch.script(), Han | Tangut | Yi)
-}
-
-fn is_grapheme_ideographic(text: &str) -> bool {
- text.chars().any(is_char_ideographic)
-}
-
-fn is_grapheme_whitespace(text: &str) -> bool {
- text.chars().any(|x| x.is_whitespace())
-}
-
-fn should_stay_with_preceding_ideograph(text: &str) -> bool {
- text.chars()
- .next()
- .is_some_and(|ch| matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…'))
-}
-
-#[derive(PartialEq, Eq, Debug, Clone, Copy)]
-enum WordBreakToken<'a> {
- Word { token: &'a str, grapheme_len: usize },
- InlineWhitespace { token: &'a str, grapheme_len: usize },
- Newline,
-}
-
-impl<'a> Iterator for WordBreakingTokenizer<'a> {
- /// Yields a span, the count of graphemes in the token, and whether it was
- /// whitespace. Note that it also breaks at word boundaries.
- type Item = WordBreakToken<'a>;
-
- fn next(&mut self) -> Option<Self::Item> {
- use unicode_segmentation::UnicodeSegmentation;
- if self.input.is_empty() {
- return None;
- }
-
- let mut iter = self.input.graphemes(true).peekable();
- let mut offset = 0;
- let mut grapheme_len = 0;
- if let Some(first_grapheme) = iter.next() {
- let is_newline = first_grapheme == "\n";
- let is_whitespace = is_grapheme_whitespace(first_grapheme);
- offset += first_grapheme.len();
- grapheme_len += 1;
- if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
- if let Some(grapheme) = iter.peek().copied()
- && should_stay_with_preceding_ideograph(grapheme)
- {
- offset += grapheme.len();
- grapheme_len += 1;
- }
- } else {
- let mut words = self.input[offset..].split_word_bound_indices().peekable();
- let mut next_word_bound = words.peek().copied();
- if next_word_bound.is_some_and(|(i, _)| i == 0) {
- next_word_bound = words.next();
- }
- while let Some(grapheme) = iter.peek().copied() {
- if next_word_bound.is_some_and(|(i, _)| i == offset) {
- break;
- };
- if is_grapheme_whitespace(grapheme) != is_whitespace
- || (grapheme == "\n") != is_newline
- {
- break;
- };
- offset += grapheme.len();
- grapheme_len += 1;
- iter.next();
- }
- }
- let token = &self.input[..offset];
- self.input = &self.input[offset..];
- if token == "\n" {
- Some(WordBreakToken::Newline)
- } else if is_whitespace {
- Some(WordBreakToken::InlineWhitespace {
- token,
- grapheme_len,
- })
- } else {
- Some(WordBreakToken::Word {
- token,
- grapheme_len,
- })
- }
- } else {
- None
- }
- }
-}
-
-#[test]
-fn test_word_breaking_tokenizer() {
- let tests: &[(&str, &[WordBreakToken<'static>])] = &[
- ("", &[]),
- (" ", &[whitespace(" ", 2)]),
- ("Ʒ", &[word("Ʒ", 1)]),
- ("Ǽ", &[word("Ǽ", 1)]),
- ("⋑", &[word("⋑", 1)]),
- ("⋑⋑", &[word("⋑⋑", 2)]),
- (
- "原理,进而",
- &[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
- ),
- (
- "hello world",
- &[word("hello", 5), whitespace(" ", 1), word("world", 5)],
- ),
- (
- "hello, world",
- &[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
- ),
- (
- " hello world",
- &[
- whitespace(" ", 2),
- word("hello", 5),
- whitespace(" ", 1),
- word("world", 5),
- ],
- ),
- (
- "这是什么 \n 钢笔",
- &[
- word("这", 1),
- word("是", 1),
- word("什", 1),
- word("么", 1),
- whitespace(" ", 1),
- newline(),
- whitespace(" ", 1),
- word("钢", 1),
- word("笔", 1),
- ],
- ),
- (" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
- ];
-
- fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
- WordBreakToken::Word {
- token,
- grapheme_len,
- }
- }
-
- fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
- WordBreakToken::InlineWhitespace {
- token,
- grapheme_len,
- }
- }
-
- fn newline() -> WordBreakToken<'static> {
- WordBreakToken::Newline
- }
-
- for (input, result) in tests {
- assert_eq!(
- WordBreakingTokenizer::new(input)
- .collect::<Vec<_>>()
- .as_slice(),
- *result,
- );
- }
-}
-
-fn wrap_with_prefix(
- first_line_prefix: String,
- subsequent_lines_prefix: String,
- unwrapped_text: String,
- wrap_column: usize,
- tab_size: NonZeroU32,
- preserve_existing_whitespace: bool,
-) -> String {
- let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size);
- let subsequent_lines_prefix_len =
- char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size);
- let mut wrapped_text = String::new();
- let mut current_line = first_line_prefix;
- let mut is_first_line = true;
-
- let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
- let mut current_line_len = first_line_prefix_len;
- let mut in_whitespace = false;
- for token in tokenizer {
- let have_preceding_whitespace = in_whitespace;
- match token {
- WordBreakToken::Word {
- token,
- grapheme_len,
- } => {
- in_whitespace = false;
- let current_prefix_len = if is_first_line {
- first_line_prefix_len
- } else {
- subsequent_lines_prefix_len
- };
- if current_line_len + grapheme_len > wrap_column
- && current_line_len != current_prefix_len
- {
- wrapped_text.push_str(current_line.trim_end());
- wrapped_text.push('\n');
- is_first_line = false;
- current_line = subsequent_lines_prefix.clone();
- current_line_len = subsequent_lines_prefix_len;
- }
- current_line.push_str(token);
- current_line_len += grapheme_len;
- }
- WordBreakToken::InlineWhitespace {
- mut token,
- mut grapheme_len,
- } => {
- in_whitespace = true;
- if have_preceding_whitespace && !preserve_existing_whitespace {
- continue;
- }
- if !preserve_existing_whitespace {
- // Keep a single whitespace grapheme as-is
- if let Some(first) =
- unicode_segmentation::UnicodeSegmentation::graphemes(token, true).next()
- {
- token = first;
- } else {
- token = " ";
- }
- grapheme_len = 1;
- }
- let current_prefix_len = if is_first_line {
- first_line_prefix_len
- } else {
- subsequent_lines_prefix_len
- };
- if current_line_len + grapheme_len > wrap_column {
- wrapped_text.push_str(current_line.trim_end());
- wrapped_text.push('\n');
- is_first_line = false;
- current_line = subsequent_lines_prefix.clone();
- current_line_len = subsequent_lines_prefix_len;
- } else if current_line_len != current_prefix_len || preserve_existing_whitespace {
- current_line.push_str(token);
- current_line_len += grapheme_len;
- }
- }
- WordBreakToken::Newline => {
- in_whitespace = true;
- let current_prefix_len = if is_first_line {
- first_line_prefix_len
- } else {
- subsequent_lines_prefix_len
- };
- if preserve_existing_whitespace {
- wrapped_text.push_str(current_line.trim_end());
- wrapped_text.push('\n');
- is_first_line = false;
- current_line = subsequent_lines_prefix.clone();
- current_line_len = subsequent_lines_prefix_len;
- } else if have_preceding_whitespace {
- continue;
- } else if current_line_len + 1 > wrap_column
- && current_line_len != current_prefix_len
- {
- wrapped_text.push_str(current_line.trim_end());
- wrapped_text.push('\n');
- is_first_line = false;
- current_line = subsequent_lines_prefix.clone();
- current_line_len = subsequent_lines_prefix_len;
- } else if current_line_len != current_prefix_len {
- current_line.push(' ');
- current_line_len += 1;
- }
- }
- }
- }
-
- if !current_line.is_empty() {
- wrapped_text.push_str(¤t_line);
- }
- wrapped_text
-}
-
-#[test]
-fn test_wrap_with_prefix() {
- assert_eq!(
- wrap_with_prefix(
- "# ".to_string(),
- "# ".to_string(),
- "abcdefg".to_string(),
- 4,
- NonZeroU32::new(4).unwrap(),
- false,
- ),
- "# abcdefg"
- );
- assert_eq!(
- wrap_with_prefix(
- "".to_string(),
- "".to_string(),
- "\thello world".to_string(),
- 8,
- NonZeroU32::new(4).unwrap(),
- false,
- ),
- "hello\nworld"
- );
- assert_eq!(
- wrap_with_prefix(
- "// ".to_string(),
- "// ".to_string(),
- "xx \nyy zz aa bb cc".to_string(),
- 12,
- NonZeroU32::new(4).unwrap(),
- false,
- ),
- "// xx yy zz\n// aa bb cc"
- );
- assert_eq!(
- wrap_with_prefix(
- String::new(),
- String::new(),
- "这是什么 \n 钢笔".to_string(),
- 3,
- NonZeroU32::new(4).unwrap(),
- false,
- ),
- "这是什\n么 钢\n笔"
- );
- assert_eq!(
- wrap_with_prefix(
- String::new(),
- String::new(),
- format!("foo{}bar", '\u{2009}'), // thin space
- 80,
- NonZeroU32::new(4).unwrap(),
- false,
- ),
- format!("foo{}bar", '\u{2009}')
- );
-}
-
pub trait CollaborationHub {
fn collaborators<'a>(&self, cx: &'a App) -> &'a HashMap<PeerId, Collaborator>;
fn user_participant_indices<'a>(&self, cx: &'a App) -> &'a HashMap<u64, ParticipantIndex>;
@@ -8173,7 +8173,7 @@ async fn test_rewrap(cx: &mut TestAppContext) {
) {
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(unwrapped_text);
- cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
+ cx.update_editor(|e, _, cx| e.rewrap(RewrapOptions::default(), cx));
cx.assert_editor_state(wrapped_text);
}
}
@@ -8578,7 +8578,7 @@ async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
) {
cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
cx.set_state(unwrapped_text);
- cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
+ cx.update_editor(|e, _, cx| e.rewrap(RewrapOptions::default(), cx));
cx.assert_editor_state(wrapped_text);
}
}
@@ -8604,7 +8604,7 @@ async fn test_rewrap_line_comment_in_go(cx: &mut TestAppContext) {
cx.set_state(indoc! {"
// Lorem ipsum dolor sit amet, consectetur adipiscing elit.ˇ
"});
- cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
+ cx.update_editor(|e, _, cx| e.rewrap(RewrapOptions::default(), cx));
cx.assert_editor_state(indoc! {"
// Lorem ipsum dolor sit amet,
// consectetur adipiscing elit.ˇ
@@ -8632,7 +8632,7 @@ async fn test_rewrap_line_comment_in_c(cx: &mut TestAppContext) {
cx.set_state(indoc! {"
// Lorem ipsum dolor sit amet, consectetur adipiscing elit.ˇ
"});
- cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
+ cx.update_editor(|e, _, cx| e.rewrap(RewrapOptions::default(), cx));
cx.assert_editor_state(indoc! {"
// Lorem ipsum dolor sit amet,
// consectetur adipiscing elit.ˇ
@@ -570,7 +570,9 @@ impl EditorElement {
register_action(editor, window, Editor::move_line_up);
register_action(editor, window, Editor::move_line_down);
register_action(editor, window, Editor::transpose);
- register_action(editor, window, Editor::rewrap);
+ register_action(editor, window, |editor, _: &crate::Rewrap, _, cx| {
+ editor.rewrap(crate::RewrapOptions::default(), cx);
+ });
register_action(editor, window, Editor::cut);
register_action(editor, window, Editor::kill_ring_cut);
register_action(editor, window, Editor::kill_ring_yank);
@@ -0,0 +1,782 @@
+use super::*;
+
+impl Editor {
+ pub fn rewrap(&mut self, options: RewrapOptions, cx: &mut Context<Self>) {
+ if self.read_only(cx) || self.mode.is_single_line() {
+ return;
+ }
+ let buffer = self.buffer.read(cx).snapshot(cx);
+ let selections = self.selections.all::<Point>(&self.display_snapshot(cx));
+
+ #[derive(Clone, Debug, PartialEq)]
+ enum CommentFormat {
+ /// single line comment, with prefix for line
+ Line(String),
+ /// single line within a block comment, with prefix for line
+ BlockLine(String),
+ /// a single line of a block comment that includes the initial delimiter
+ BlockCommentWithStart(BlockCommentConfig),
+ /// a single line of a block comment that includes the ending delimiter
+ BlockCommentWithEnd(BlockCommentConfig),
+ }
+
+ // Split selections to respect paragraph, indent, and comment prefix boundaries.
+ let wrap_ranges = selections.into_iter().flat_map(|selection| {
+ let language_settings = buffer.language_settings_at(selection.head(), cx);
+ let language_scope = buffer.language_scope_at(selection.head());
+
+ let indent_and_prefix_for_row =
+ |row: u32| -> (IndentSize, Option<CommentFormat>, Option<String>) {
+ let indent = buffer.indent_size_for_line(MultiBufferRow(row));
+ let (comment_prefix, rewrap_prefix) = if let Some(language_scope) =
+ &language_scope
+ {
+ let indent_end = Point::new(row, indent.len);
+ let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
+ let line_text_after_indent = buffer
+ .text_for_range(indent_end..line_end)
+ .collect::<String>();
+
+ let is_within_comment_override = buffer
+ .language_scope_at(indent_end)
+ .is_some_and(|scope| scope.override_name() == Some("comment"));
+ let comment_delimiters = if is_within_comment_override {
+ // we are within a comment syntax node, but we don't
+ // yet know what kind of comment: block, doc or line
+ match (
+ language_scope.documentation_comment(),
+ language_scope.block_comment(),
+ ) {
+ (Some(config), _) | (_, Some(config))
+ if buffer.contains_str_at(indent_end, &config.start) =>
+ {
+ Some(CommentFormat::BlockCommentWithStart(config.clone()))
+ }
+ (Some(config), _) | (_, Some(config))
+ if line_text_after_indent.ends_with(config.end.as_ref()) =>
+ {
+ Some(CommentFormat::BlockCommentWithEnd(config.clone()))
+ }
+ (Some(config), _) | (_, Some(config))
+ if !config.prefix.is_empty()
+ && buffer.contains_str_at(indent_end, &config.prefix) =>
+ {
+ Some(CommentFormat::BlockLine(config.prefix.to_string()))
+ }
+ (_, _) => language_scope
+ .line_comment_prefixes()
+ .iter()
+ .find(|prefix| buffer.contains_str_at(indent_end, prefix))
+ .map(|prefix| CommentFormat::Line(prefix.to_string())),
+ }
+ } else {
+ // we not in an overridden comment node, but we may
+ // be within a non-overridden line comment node
+ language_scope
+ .line_comment_prefixes()
+ .iter()
+ .find(|prefix| buffer.contains_str_at(indent_end, prefix))
+ .map(|prefix| CommentFormat::Line(prefix.to_string()))
+ };
+
+ let rewrap_prefix = language_scope
+ .rewrap_prefixes()
+ .iter()
+ .find_map(|prefix_regex| {
+ prefix_regex.find(&line_text_after_indent).map(|mat| {
+ if mat.start() == 0 {
+ Some(mat.as_str().to_string())
+ } else {
+ None
+ }
+ })
+ })
+ .flatten();
+ (comment_delimiters, rewrap_prefix)
+ } else {
+ (None, None)
+ };
+ (indent, comment_prefix, rewrap_prefix)
+ };
+
+ let mut start_row = selection.start.row;
+ let mut end_row = selection.end.row;
+
+ if selection.is_empty() {
+ let cursor_row = selection.start.row;
+
+ let (mut indent_size, comment_prefix, _) = indent_and_prefix_for_row(cursor_row);
+ let line_prefix = match &comment_prefix {
+ Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
+ Some(prefix.as_str())
+ }
+ Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
+ prefix, ..
+ })) => Some(prefix.as_ref()),
+ Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
+ start: _,
+ end: _,
+ prefix,
+ tab_size,
+ })) => {
+ indent_size.len += tab_size;
+ Some(prefix.as_ref())
+ }
+ None => None,
+ };
+ let indent_prefix = indent_size.chars().collect::<String>();
+ let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
+
+ '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()
+ && !buffer.is_line_blank(MultiBufferRow(prev_row))
+ {
+ 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()
+ && !buffer.is_line_blank(MultiBufferRow(next_row))
+ {
+ end_row = next_row;
+ } else {
+ break 'expand_downwards;
+ }
+ }
+ }
+
+ let mut non_blank_rows_iter = (start_row..=end_row)
+ .filter(|row| !buffer.is_line_blank(MultiBufferRow(*row)))
+ .peekable();
+
+ let first_row = if let Some(&row) = non_blank_rows_iter.peek() {
+ row
+ } else {
+ return Vec::new();
+ };
+
+ let mut ranges = Vec::new();
+
+ let mut current_range_start = first_row;
+ let mut prev_row = first_row;
+ let (
+ mut current_range_indent,
+ mut current_range_comment_delimiters,
+ mut current_range_rewrap_prefix,
+ ) = indent_and_prefix_for_row(first_row);
+
+ for row in non_blank_rows_iter.skip(1) {
+ let has_paragraph_break = row > prev_row + 1;
+
+ let (row_indent, row_comment_delimiters, row_rewrap_prefix) =
+ indent_and_prefix_for_row(row);
+
+ let has_indent_change = row_indent != current_range_indent;
+ let has_comment_change = row_comment_delimiters != current_range_comment_delimiters;
+
+ let has_boundary_change = has_comment_change
+ || row_rewrap_prefix.is_some()
+ || (has_indent_change && current_range_comment_delimiters.is_some());
+
+ if has_paragraph_break || has_boundary_change {
+ ranges.push((
+ language_settings.clone(),
+ Point::new(current_range_start, 0)
+ ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
+ current_range_indent,
+ current_range_comment_delimiters.clone(),
+ current_range_rewrap_prefix.clone(),
+ ));
+ current_range_start = row;
+ current_range_indent = row_indent;
+ current_range_comment_delimiters = row_comment_delimiters;
+ current_range_rewrap_prefix = row_rewrap_prefix;
+ }
+ prev_row = row;
+ }
+
+ ranges.push((
+ language_settings.clone(),
+ Point::new(current_range_start, 0)
+ ..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
+ current_range_indent,
+ current_range_comment_delimiters,
+ current_range_rewrap_prefix,
+ ));
+
+ ranges
+ });
+
+ let mut edits = Vec::new();
+ let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
+
+ for (language_settings, wrap_range, mut indent_size, comment_prefix, rewrap_prefix) in
+ wrap_ranges
+ {
+ let start_row = wrap_range.start.row;
+ let end_row = wrap_range.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 tab_size = language_settings.tab_size;
+
+ let (line_prefix, inside_comment) = match &comment_prefix {
+ Some(CommentFormat::Line(prefix) | CommentFormat::BlockLine(prefix)) => {
+ (Some(prefix.as_str()), true)
+ }
+ Some(CommentFormat::BlockCommentWithEnd(BlockCommentConfig { prefix, .. })) => {
+ (Some(prefix.as_ref()), true)
+ }
+ Some(CommentFormat::BlockCommentWithStart(BlockCommentConfig {
+ start: _,
+ end: _,
+ prefix,
+ tab_size,
+ })) => {
+ indent_size.len += tab_size;
+ (Some(prefix.as_ref()), true)
+ }
+ None => (None, false),
+ };
+ let indent_prefix = indent_size.chars().collect::<String>();
+ let line_prefix = format!("{indent_prefix}{}", line_prefix.unwrap_or(""));
+
+ let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
+ RewrapBehavior::InComments => inside_comment,
+ RewrapBehavior::InSelections => !wrap_range.is_empty(),
+ RewrapBehavior::Anywhere => true,
+ };
+
+ let should_rewrap = options.override_language_settings
+ || allow_rewrap_based_on_language
+ || self.hard_wrap.is_some();
+ if !should_rewrap {
+ continue;
+ }
+
+ let start = Point::new(start_row, 0);
+ let start_offset = ToOffset::to_offset(&start, &buffer);
+ let end = Point::new(end_row, buffer.line_len(MultiBufferRow(end_row)));
+ let selection_text = buffer.text_for_range(start..end).collect::<String>();
+ let mut first_line_delimiter = None;
+ let mut last_line_delimiter = None;
+ let Some(lines_without_prefixes) = selection_text
+ .lines()
+ .enumerate()
+ .map(|(ix, line)| {
+ let line_trimmed = line.trim_start();
+ if rewrap_prefix.is_some() && ix > 0 {
+ Ok(line_trimmed)
+ } else if let Some(
+ CommentFormat::BlockCommentWithStart(BlockCommentConfig {
+ start,
+ prefix,
+ end,
+ tab_size,
+ })
+ | CommentFormat::BlockCommentWithEnd(BlockCommentConfig {
+ start,
+ prefix,
+ end,
+ tab_size,
+ }),
+ ) = &comment_prefix
+ {
+ let line_trimmed = line_trimmed
+ .strip_prefix(start.as_ref())
+ .map(|s| {
+ let mut indent_size = indent_size;
+ indent_size.len -= tab_size;
+ let indent_prefix: String = indent_size.chars().collect();
+ first_line_delimiter = Some((indent_prefix, start));
+ s.trim_start()
+ })
+ .unwrap_or(line_trimmed);
+ let line_trimmed = line_trimmed
+ .strip_suffix(end.as_ref())
+ .map(|s| {
+ last_line_delimiter = Some(end);
+ s.trim_end()
+ })
+ .unwrap_or(line_trimmed);
+ let line_trimmed = line_trimmed
+ .strip_prefix(prefix.as_ref())
+ .unwrap_or(line_trimmed);
+ Ok(line_trimmed)
+ } else if let Some(CommentFormat::BlockLine(prefix)) = &comment_prefix {
+ line_trimmed.strip_prefix(prefix).with_context(|| {
+ format!("line did not start with prefix {prefix:?}: {line:?}")
+ })
+ } else {
+ line_trimmed
+ .strip_prefix(&line_prefix.trim_start())
+ .with_context(|| {
+ format!("line did not start with prefix {line_prefix:?}: {line:?}")
+ })
+ }
+ })
+ .collect::<Result<Vec<_>, _>>()
+ .log_err()
+ else {
+ continue;
+ };
+
+ let wrap_column = options.line_length.or(self.hard_wrap).unwrap_or_else(|| {
+ buffer
+ .language_settings_at(Point::new(start_row, 0), cx)
+ .preferred_line_length as usize
+ });
+
+ let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
+ format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
+ } else {
+ line_prefix.clone()
+ };
+
+ let wrapped_text = {
+ let mut wrapped_text = wrap_with_prefix(
+ line_prefix,
+ subsequent_lines_prefix,
+ lines_without_prefixes.join("\n"),
+ wrap_column,
+ tab_size,
+ options.preserve_existing_whitespace,
+ );
+
+ if let Some((indent, delimiter)) = first_line_delimiter {
+ wrapped_text = format!("{indent}{delimiter}\n{wrapped_text}");
+ }
+ if let Some(last_line) = last_line_delimiter {
+ wrapped_text = format!("{wrapped_text}\n{indent_prefix}{last_line}");
+ }
+
+ wrapped_text
+ };
+
+ // TODO: should always use char-based diff while still supporting cursor behavior that
+ // matches vim.
+ let mut diff_options = DiffOptions::default();
+ if options.override_language_settings {
+ diff_options.max_word_diff_len = 0;
+ diff_options.max_word_diff_line_count = 0;
+ } else {
+ diff_options.max_word_diff_len = usize::MAX;
+ diff_options.max_word_diff_line_count = usize::MAX;
+ }
+
+ for (old_range, new_text) in
+ text_diff_with_options(&selection_text, &wrapped_text, diff_options)
+ {
+ let edit_start = buffer.anchor_after(start_offset + old_range.start);
+ let edit_end = buffer.anchor_after(start_offset + old_range.end);
+ edits.push((edit_start..edit_end, new_text));
+ }
+
+ rewrapped_row_ranges.push(start_row..=end_row);
+ }
+
+ self.buffer
+ .update(cx, |buffer, cx| buffer.edit(edits, None, cx));
+ }
+}
+
+fn char_len_with_expanded_tabs(offset: usize, text: &str, tab_size: NonZeroU32) -> usize {
+ let tab_size = tab_size.get() as usize;
+ let mut width = offset;
+
+ for ch in text.chars() {
+ width += if ch == '\t' {
+ tab_size - (width % tab_size)
+ } else {
+ 1
+ };
+ }
+
+ width - offset
+}
+
+/// Tokenizes a string into runs of text that should stick together, or that is whitespace.
+struct WordBreakingTokenizer<'a> {
+ input: &'a str,
+}
+
+impl<'a> WordBreakingTokenizer<'a> {
+ fn new(input: &'a str) -> Self {
+ Self { input }
+ }
+}
+
+fn is_char_ideographic(ch: char) -> bool {
+ use unicode_script::Script::*;
+ use unicode_script::UnicodeScript;
+ matches!(ch.script(), Han | Tangut | Yi)
+}
+
+fn is_grapheme_ideographic(text: &str) -> bool {
+ text.chars().any(is_char_ideographic)
+}
+
+fn is_grapheme_whitespace(text: &str) -> bool {
+ text.chars().any(|x| x.is_whitespace())
+}
+
+fn should_stay_with_preceding_ideograph(text: &str) -> bool {
+ text.chars()
+ .next()
+ .is_some_and(|ch| matches!(ch, '。' | '、' | ',' | '?' | '!' | ':' | ';' | '…'))
+}
+
+#[derive(PartialEq, Eq, Debug, Clone, Copy)]
+enum WordBreakToken<'a> {
+ Word { token: &'a str, grapheme_len: usize },
+ InlineWhitespace { token: &'a str, grapheme_len: usize },
+ Newline,
+}
+
+impl<'a> Iterator for WordBreakingTokenizer<'a> {
+ /// Yields a span, the count of graphemes in the token, and whether it was
+ /// whitespace. Note that it also breaks at word boundaries.
+ type Item = WordBreakToken<'a>;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ use unicode_segmentation::UnicodeSegmentation;
+ if self.input.is_empty() {
+ return None;
+ }
+
+ let mut iter = self.input.graphemes(true).peekable();
+ let mut offset = 0;
+ let mut grapheme_len = 0;
+ if let Some(first_grapheme) = iter.next() {
+ let is_newline = first_grapheme == "\n";
+ let is_whitespace = is_grapheme_whitespace(first_grapheme);
+ offset += first_grapheme.len();
+ grapheme_len += 1;
+ if is_grapheme_ideographic(first_grapheme) && !is_whitespace {
+ if let Some(grapheme) = iter.peek().copied()
+ && should_stay_with_preceding_ideograph(grapheme)
+ {
+ offset += grapheme.len();
+ grapheme_len += 1;
+ }
+ } else {
+ let mut words = self.input[offset..].split_word_bound_indices().peekable();
+ let mut next_word_bound = words.peek().copied();
+ if next_word_bound.is_some_and(|(i, _)| i == 0) {
+ next_word_bound = words.next();
+ }
+ while let Some(grapheme) = iter.peek().copied() {
+ if next_word_bound.is_some_and(|(i, _)| i == offset) {
+ break;
+ };
+ if is_grapheme_whitespace(grapheme) != is_whitespace
+ || (grapheme == "\n") != is_newline
+ {
+ break;
+ };
+ offset += grapheme.len();
+ grapheme_len += 1;
+ iter.next();
+ }
+ }
+ let token = &self.input[..offset];
+ self.input = &self.input[offset..];
+ if token == "\n" {
+ Some(WordBreakToken::Newline)
+ } else if is_whitespace {
+ Some(WordBreakToken::InlineWhitespace {
+ token,
+ grapheme_len,
+ })
+ } else {
+ Some(WordBreakToken::Word {
+ token,
+ grapheme_len,
+ })
+ }
+ } else {
+ None
+ }
+ }
+}
+
+fn wrap_with_prefix(
+ first_line_prefix: String,
+ subsequent_lines_prefix: String,
+ unwrapped_text: String,
+ wrap_column: usize,
+ tab_size: NonZeroU32,
+ preserve_existing_whitespace: bool,
+) -> String {
+ let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size);
+ let subsequent_lines_prefix_len =
+ char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size);
+ let mut wrapped_text = String::new();
+ let mut current_line = first_line_prefix;
+ let mut is_first_line = true;
+
+ let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
+ let mut current_line_len = first_line_prefix_len;
+ let mut in_whitespace = false;
+ for token in tokenizer {
+ let have_preceding_whitespace = in_whitespace;
+ match token {
+ WordBreakToken::Word {
+ token,
+ grapheme_len,
+ } => {
+ in_whitespace = false;
+ let current_prefix_len = if is_first_line {
+ first_line_prefix_len
+ } else {
+ subsequent_lines_prefix_len
+ };
+ if current_line_len + grapheme_len > wrap_column
+ && current_line_len != current_prefix_len
+ {
+ wrapped_text.push_str(current_line.trim_end());
+ wrapped_text.push('\n');
+ is_first_line = false;
+ current_line = subsequent_lines_prefix.clone();
+ current_line_len = subsequent_lines_prefix_len;
+ }
+ current_line.push_str(token);
+ current_line_len += grapheme_len;
+ }
+ WordBreakToken::InlineWhitespace {
+ mut token,
+ mut grapheme_len,
+ } => {
+ in_whitespace = true;
+ if have_preceding_whitespace && !preserve_existing_whitespace {
+ continue;
+ }
+ if !preserve_existing_whitespace {
+ // Keep a single whitespace grapheme as-is
+ if let Some(first) =
+ unicode_segmentation::UnicodeSegmentation::graphemes(token, true).next()
+ {
+ token = first;
+ } else {
+ token = " ";
+ }
+ grapheme_len = 1;
+ }
+ let current_prefix_len = if is_first_line {
+ first_line_prefix_len
+ } else {
+ subsequent_lines_prefix_len
+ };
+ if current_line_len + grapheme_len > wrap_column {
+ wrapped_text.push_str(current_line.trim_end());
+ wrapped_text.push('\n');
+ is_first_line = false;
+ current_line = subsequent_lines_prefix.clone();
+ current_line_len = subsequent_lines_prefix_len;
+ } else if current_line_len != current_prefix_len || preserve_existing_whitespace {
+ current_line.push_str(token);
+ current_line_len += grapheme_len;
+ }
+ }
+ WordBreakToken::Newline => {
+ in_whitespace = true;
+ let current_prefix_len = if is_first_line {
+ first_line_prefix_len
+ } else {
+ subsequent_lines_prefix_len
+ };
+ if preserve_existing_whitespace {
+ wrapped_text.push_str(current_line.trim_end());
+ wrapped_text.push('\n');
+ is_first_line = false;
+ current_line = subsequent_lines_prefix.clone();
+ current_line_len = subsequent_lines_prefix_len;
+ } else if have_preceding_whitespace {
+ continue;
+ } else if current_line_len + 1 > wrap_column
+ && current_line_len != current_prefix_len
+ {
+ wrapped_text.push_str(current_line.trim_end());
+ wrapped_text.push('\n');
+ is_first_line = false;
+ current_line = subsequent_lines_prefix.clone();
+ current_line_len = subsequent_lines_prefix_len;
+ } else if current_line_len != current_prefix_len {
+ current_line.push(' ');
+ current_line_len += 1;
+ }
+ }
+ }
+ }
+
+ if !current_line.is_empty() {
+ wrapped_text.push_str(¤t_line);
+ }
+ wrapped_text
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_string_size_with_expanded_tabs() {
+ let nz = |val| NonZeroU32::new(val).unwrap();
+ assert_eq!(char_len_with_expanded_tabs(0, "", nz(4)), 0);
+ assert_eq!(char_len_with_expanded_tabs(0, "hello", nz(4)), 5);
+ assert_eq!(char_len_with_expanded_tabs(0, "\thello", nz(4)), 9);
+ assert_eq!(char_len_with_expanded_tabs(0, "abc\tab", nz(4)), 6);
+ assert_eq!(char_len_with_expanded_tabs(0, "hello\t", nz(4)), 8);
+ assert_eq!(char_len_with_expanded_tabs(0, "\t\t", nz(8)), 16);
+ assert_eq!(char_len_with_expanded_tabs(0, "x\t", nz(8)), 8);
+ assert_eq!(char_len_with_expanded_tabs(7, "x\t", nz(8)), 9);
+ }
+
+ #[test]
+ fn test_word_breaking_tokenizer() {
+ let tests: &[(&str, &[WordBreakToken<'static>])] = &[
+ ("", &[]),
+ (" ", &[whitespace(" ", 2)]),
+ ("Ʒ", &[word("Ʒ", 1)]),
+ ("Ǽ", &[word("Ǽ", 1)]),
+ ("⋑", &[word("⋑", 1)]),
+ ("⋑⋑", &[word("⋑⋑", 2)]),
+ (
+ "原理,进而",
+ &[word("原", 1), word("理,", 2), word("进", 1), word("而", 1)],
+ ),
+ (
+ "hello world",
+ &[word("hello", 5), whitespace(" ", 1), word("world", 5)],
+ ),
+ (
+ "hello, world",
+ &[word("hello,", 6), whitespace(" ", 1), word("world", 5)],
+ ),
+ (
+ " hello world",
+ &[
+ whitespace(" ", 2),
+ word("hello", 5),
+ whitespace(" ", 1),
+ word("world", 5),
+ ],
+ ),
+ (
+ "这是什么 \n 钢笔",
+ &[
+ word("这", 1),
+ word("是", 1),
+ word("什", 1),
+ word("么", 1),
+ whitespace(" ", 1),
+ newline(),
+ whitespace(" ", 1),
+ word("钢", 1),
+ word("笔", 1),
+ ],
+ ),
+ (" mutton", &[whitespace(" ", 1), word("mutton", 6)]),
+ ];
+
+ fn word(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
+ WordBreakToken::Word {
+ token,
+ grapheme_len,
+ }
+ }
+
+ fn whitespace(token: &'static str, grapheme_len: usize) -> WordBreakToken<'static> {
+ WordBreakToken::InlineWhitespace {
+ token,
+ grapheme_len,
+ }
+ }
+
+ fn newline() -> WordBreakToken<'static> {
+ WordBreakToken::Newline
+ }
+
+ for (input, result) in tests {
+ assert_eq!(
+ WordBreakingTokenizer::new(input)
+ .collect::<Vec<_>>()
+ .as_slice(),
+ *result,
+ );
+ }
+ }
+
+ #[test]
+ fn test_wrap_with_prefix() {
+ assert_eq!(
+ wrap_with_prefix(
+ "# ".to_string(),
+ "# ".to_string(),
+ "abcdefg".to_string(),
+ 4,
+ NonZeroU32::new(4).unwrap(),
+ false,
+ ),
+ "# abcdefg"
+ );
+ assert_eq!(
+ wrap_with_prefix(
+ "".to_string(),
+ "".to_string(),
+ "\thello world".to_string(),
+ 8,
+ NonZeroU32::new(4).unwrap(),
+ false,
+ ),
+ "hello\nworld"
+ );
+ assert_eq!(
+ wrap_with_prefix(
+ "// ".to_string(),
+ "// ".to_string(),
+ "xx \nyy zz aa bb cc".to_string(),
+ 12,
+ NonZeroU32::new(4).unwrap(),
+ false,
+ ),
+ "// xx yy zz\n// aa bb cc"
+ );
+ assert_eq!(
+ wrap_with_prefix(
+ String::new(),
+ String::new(),
+ "这是什么 \n 钢笔".to_string(),
+ 3,
+ NonZeroU32::new(4).unwrap(),
+ false,
+ ),
+ "这是什\n么 钢\n笔"
+ );
+ assert_eq!(
+ wrap_with_prefix(
+ String::new(),
+ String::new(),
+ format!("foo{}bar", '\u{2009}'), // thin space
+ 80,
+ NonZeroU32::new(4).unwrap(),
+ false,
+ ),
+ format!("foo{}bar", '\u{2009}')
+ );
+ }
+}
@@ -2244,7 +2244,7 @@ impl GitPanel {
let editor = cx.new(|cx| Editor::for_buffer(buffer, None, window, cx));
let wrapped_message = editor.update(cx, |editor, cx| {
editor.select_all(&Default::default(), window, cx);
- editor.rewrap_impl(
+ editor.rewrap(
RewrapOptions {
override_language_settings: false,
preserve_existing_whitespace: true,
@@ -22,7 +22,7 @@ pub(crate) fn register(editor: &mut Editor, cx: &mut Context<Vim>) {
vim.update_editor(cx, |vim, editor, cx| {
editor.transact(window, cx, |editor, window, cx| {
let mut positions = vim.save_selection_starts(editor, cx);
- editor.rewrap_impl(
+ editor.rewrap(
RewrapOptions {
override_language_settings: true,
line_length: action.line_length,
@@ -74,7 +74,7 @@ impl Vim {
);
});
});
- editor.rewrap_impl(
+ editor.rewrap(
RewrapOptions {
override_language_settings: true,
..Default::default()
@@ -112,7 +112,7 @@ impl Vim {
object.expand_selection(map, selection, around, times);
});
});
- editor.rewrap_impl(
+ editor.rewrap(
RewrapOptions {
override_language_settings: true,
..Default::default()