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

Code before:
fn extend_line_above(cx: &mut Context) {
    extend_line_impl(cx, Extend::Above);
}
fn select_line_down(cx: &mut Context) {
    select_line_impl(cx, Extend::Below);
}
fn select_line_up(cx: &mut Context) {
    select_line_impl(cx, Extend::Above);
}
fn select_line_impl(cx: &mut Context, extend: Extend) {
    let count = cx.count();
    let (view, doc) = current!(cx.editor);

    let text = doc.text();
    let selection = doc.selection(view.id).clone().transform(|range| {
        let (start_line, end_line) = range.line_range(text.slice(..));

        let start = text.line_to_char(start_line);
        let end = text.line_to_char(
            (end_line + 1) // newline of end_line
                .min(text.len_lines()),
        );
        let direction = range.direction();
        // extend or shrink to previous/next line if current line is selected
        // extending or shrinking depends on the current direction of the selection
        let (anchor, head) = if range.from() == start && range.to() == end {
            match extend {
                Extend::Above => match direction {
                    Direction::Forward => {
                        (start, text.line_to_char(end_line.saturating_sub(count - 1)))
                    }
                    Direction::Backward => {
                        (end, text.line_to_char(start_line.saturating_sub(count)))
                    }
                },
                Extend::Below => match direction {
                    Direction::Forward => (
                        start,
                        text.line_to_char((end_line + count + 1).min(text.len_lines())),
                    ),
                    Direction::Backward => (
                        end,
                        text.line_to_char((start_line + count).min(text.len_lines())),
                    ),
                },
            }
        } else {
            match extend {
                Extend::Above => (end, text.line_to_char(start_line.saturating_sub(count - 1))),
                Extend::Below => (
                    start,

Review comment (suggestion):
The case where you flip over the selection (e.g. JJKKK) feels a little off. I can think of two ways of making this more graceful:

IF selection does not consist of whole lines THEN expand selection to whole lines
ELSE IF selection consists of a single line and orientation is not in the direction of movement THEN change selection orientation
ELSE modify selection like you already implemented

Or alternatively, adopt the behavior from the bindings at https://github.com/helix-editor/helix/issues/165#issuecomment-1817779737

Code after:

fn extend_line_above(cx: &mut Context) {
    extend_line_impl(cx, Extend::Above);
}
fn extend_line_impl(cx: &mut Context, extend: Extend) {
    let count = cx.count();
    let (view, doc) = current!(cx.editor);

    let text = doc.text();
    let selection = doc.selection(view.id).clone().transform(|range| {
        let (start_line, end_line) = range.line_range(text.slice(..));

        let start = text.line_to_char(start_line);
        let end = text.line_to_char(
            (end_line + 1) // newline of end_line
                .min(text.len_lines()),
        );

        // extend to previous/next line if current line is selected
        let (anchor, head) = if range.from() == start && range.to() == end {
            match extend {
                Extend::Above => (end, text.line_to_char(start_line.saturating_sub(count))),
                Extend::Below => (
                    start,
                    text.line_to_char((end_line + count + 1).min(text.len_lines())),
                ),
            }
        } else {
            match extend {
                Extend::Above => (end, text.line_to_char(start_line.saturating_sub(count - 1))),
                Extend::Below => (
                    start,
                    text.line_to_char((end_line + count).min(text.len_lines())),
                ),
            }
        };

        Range::new(anchor, head)
    });

    doc.set_selection(view.id, selection);
}
fn select_line_below(cx: &mut Context) {
    select_line_impl(cx, Extend::Below);
}
fn select_line_above(cx: &mut Context) {
    select_line_impl(cx, Extend::Above);
}
fn select_line_impl(cx: &mut Context, extend: Extend) {
    let mut count = cx.count();
    let (view, doc) = current!(cx.editor);