Repository: helix-editor/helix
File: helix-term/src/commands.rs (Rust)

Code before:
    use movement::skip_while;
    let (view, doc) = current!(cx.editor);

    let mut changes = Vec::new();

    for selection in doc.selection(view.id) {
        let slice = doc.text().slice(..);
        let text = doc.text();

        let selected_lines = {
            let (start, mut end) = selection.line_range(slice);
            if start == end {
                end = (end + 1).min(text.len_lines() - 1);
            }
            start..end
        };

        for selected_line in selected_lines {
            let start = line_end_char_index(&slice, selected_line);
            let mut end = text.line_to_char(selected_line + 1);
            end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);

            // need to skip from start, not end
            let change = {
                let separator = {
                    let line_contains_only_space = text.char(end) == '\n';
                    if line_contains_only_space {
                        None
                    } else {
                        Some(Tendril::from(" "))
                    }
                };

                (start, end, separator)
            };
            changes.push(change);
        }
    }

    // nothing to do, bail out early to avoid crashes later
    if changes.is_empty() {
        return;
    }

    changes.sort_unstable_by_key(|(from, _to, _text)| *from);
    changes.dedup();

    // select inserted spaces
    let transaction = if select_space {
        let ranges: SmallVec<_> = changes
            .iter()

Review comment (suggestion):
This check won't work for non-LF line ending documents. For example if you try the reproduction case from the issue with `:line-ending crlf` and then `JJ` it will still have the empty space.

Code after:
    use movement::skip_while;
    let (view, doc) = current!(cx.editor);
    let text = doc.text();
    let slice = text.slice(..);

    let mut changes = Vec::new();

    for selection in doc.selection(view.id) {
        let (start, mut end) = selection.line_range(slice);
        if start == end {
            end = (end + 1).min(text.len_lines() - 1);
        }
        let lines = start..end;

        changes.reserve(lines.len());

        for line in lines {
            let start = line_end_char_index(&slice, line);
            let mut end = text.line_to_char(line + 1);
            end = skip_while(slice, end, |ch| matches!(ch, ' ' | '\t')).unwrap_or(end);

            let separator = if end == line_end_char_index(&slice, line + 1) {
                // the joining line contains only space-characters => don't include a whitespace when joining
                None
            } else {
                Some(Tendril::from(" "))
            };
            changes.push((start, end, separator));
        }
    }

    // nothing to do, bail out early to avoid crashes later
    if changes.is_empty() {
        return;
    }

    changes.sort_unstable_by_key(|(from, _to, _text)| *from);
    changes.dedup();

    // select inserted spaces
    let transaction = if select_space {
        let ranges: SmallVec<_> = changes
            .iter()
            .scan(0, |offset, change| {
                let range = Range::point(change.0 - *offset);
                *offset += change.1 - change.0 - 1; // -1 because cursor is 0-sized
                Some(range)
            })
            .collect();
        let selection = Selection::new(ranges, 0);
        Transaction::change(text, changes.into_iter()).with_selection(selection)