diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 132425d2139e11d72b6ac70f6435abdad87e7c2d..482fd9ee1b34d475d93bd29f2c1f7ea700053937 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -11334,14 +11334,66 @@ impl Editor { let end_of_line = Point::new(row.0, snapshot.line_len(row)); let next_line_row = row.next_row(); let indent = snapshot.indent_size_for_line(next_line_row); - let start_of_next_line = Point::new(next_line_row.0, indent.len); + let mut join_start_column = indent.len; - let replace = - if snapshot.line_len(next_line_row) > indent.len && insert_whitespace { - " " - } else { - "" - }; + if let Some(language_scope) = + snapshot.language_scope_at(Point::new(next_line_row.0, indent.len)) + { + let line_end = + Point::new(next_line_row.0, snapshot.line_len(next_line_row)); + let line_text_after_indent = snapshot + .text_for_range(Point::new(next_line_row.0, indent.len)..line_end) + .collect::(); + + if !line_text_after_indent.is_empty() { + let block_prefix = language_scope + .block_comment() + .map(|c| c.prefix.as_ref()) + .filter(|p| !p.is_empty()); + let doc_prefix = language_scope + .documentation_comment() + .map(|c| c.prefix.as_ref()) + .filter(|p| !p.is_empty()); + let all_prefixes = language_scope + .line_comment_prefixes() + .iter() + .map(|p| p.as_ref()) + .chain(block_prefix) + .chain(doc_prefix) + .chain(language_scope.unordered_list().iter().map(|p| p.as_ref())); + + let mut longest_prefix_len = None; + for prefix in all_prefixes { + let trimmed = prefix.trim_end(); + if line_text_after_indent.starts_with(trimmed) { + let candidate_len = + if line_text_after_indent.starts_with(prefix) { + prefix.len() + } else { + trimmed.len() + }; + if longest_prefix_len.map_or(true, |len| candidate_len > len) { + longest_prefix_len = Some(candidate_len); + } + } + } + + if let Some(prefix_len) = longest_prefix_len { + join_start_column = + join_start_column.saturating_add(prefix_len as u32); + } + } + } + + let start_of_next_line = Point::new(next_line_row.0, join_start_column); + + let replace = if snapshot.line_len(next_line_row) > join_start_column + && insert_whitespace + { + " " + } else { + "" + }; this.buffer.update(cx, |buffer, cx| { buffer.edit([(end_of_line..start_of_next_line, replace)], None, cx) diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 6788f899d5ce0ffa933a927a2ffbc18163e75935..577cedc04a50eacaa1d207c96ca215095084d25b 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -5014,6 +5014,163 @@ async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &m ); } +#[gpui::test] +async fn test_join_lines_strips_comment_prefix(cx: &mut TestAppContext) { + init_test(cx, |_| {}); + + { + let language = Arc::new(Language::new( + LanguageConfig { + line_comments: vec!["// ".into(), "/// ".into()], + documentation_comment: Some(BlockCommentConfig { + start: "/*".into(), + end: "*/".into(), + prefix: "* ".into(), + tab_size: 1, + }), + ..LanguageConfig::default() + }, + None, + )); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx)); + + // Strips the comment prefix (with trailing space) from the joined-in line. + cx.set_state(indoc! {" + // ˇfoo + // bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + // fooˇ bar + "}); + + // Strips the longer doc-comment prefix when both `//` and `///` match. + cx.set_state(indoc! {" + /// ˇfoo + /// bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + /// fooˇ bar + "}); + + // Does not strip when the second line is a regular line (no comment prefix). + cx.set_state(indoc! {" + // ˇfoo + bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + // fooˇ bar + "}); + + // No-whitespace join also strips the comment prefix. + cx.set_state(indoc! {" + // ˇfoo + // bar + "}); + cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx)); + cx.assert_editor_state(indoc! {" + // fooˇbar + "}); + + // Strips even when the joined-in line is just the bare prefix (no trailing space). + cx.set_state(indoc! {" + // ˇfoo + // + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + // fooˇ + "}); + + // Mixed line comment prefix types: the longer matching prefix is stripped. + cx.set_state(indoc! {" + // ˇfoo + /// bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + // fooˇ bar + "}); + + // Strips block comment body prefix (`* `) from the joined-in line. + cx.set_state(indoc! {" + * ˇfoo + * bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + * fooˇ bar + "}); + + // Strips bare block comment body prefix (`*` without trailing space). + cx.set_state(indoc! {" + * ˇfoo + * + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + * fooˇ + "}); + } + + { + let markdown_language = Arc::new(Language::new( + LanguageConfig { + unordered_list: vec!["- ".into(), "* ".into(), "+ ".into()], + ..LanguageConfig::default() + }, + None, + )); + + let mut cx = EditorTestContext::new(cx).await; + cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx)); + + // Strips the `- ` list marker from the joined-in line. + cx.set_state(indoc! {" + - ˇfoo + - bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + - fooˇ bar + "}); + + // Strips the `* ` list marker from the joined-in line. + cx.set_state(indoc! {" + * ˇfoo + * bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + * fooˇ bar + "}); + + // Strips the `+ ` list marker from the joined-in line. + cx.set_state(indoc! {" + + ˇfoo + + bar + "}); + cx.update_editor(|e, window, cx| e.join_lines(&JoinLines, window, cx)); + cx.assert_editor_state(indoc! {" + + fooˇ bar + "}); + + // No-whitespace join also strips the list marker. + cx.set_state(indoc! {" + - ˇfoo + - bar + "}); + cx.update_editor(|e, window, cx| e.join_lines_impl(false, window, cx)); + cx.assert_editor_state(indoc! {" + - fooˇbar + "}); + } +} + #[gpui::test] async fn test_custom_newlines_cause_no_false_positive_diffs( executor: BackgroundExecutor,