aboutsummaryrefslogtreecommitdiff
path: root/helix-core/src/commands.rs
blob: 3bebd7b4632f4eff4209409a2ee048e9505d0333 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
use crate::graphemes;
use crate::selection::{Range, Selection};
use crate::state::{Direction, Granularity, Mode, State};
use crate::transaction::{ChangeSet, Transaction};
use crate::Tendril;

/// A command is a function that takes the current state and a count, and does a side-effect on the
/// state (usually by creating and applying a transaction).
pub type Command = fn(state: &mut State, count: usize);

pub fn move_char_left(state: &mut State, count: usize) {
    // TODO: use a transaction
    let selection = state.move_selection(Direction::Backward, Granularity::Character, count);
    state.selection = selection;
}

pub fn move_char_right(state: &mut State, count: usize) {
    // TODO: use a transaction
    state.selection = state.move_selection(Direction::Forward, Granularity::Character, count);
}

pub fn move_line_up(state: &mut State, count: usize) {
    // TODO: use a transaction
    state.selection = state.move_selection(Direction::Backward, Granularity::Line, count);
}

pub fn move_line_down(state: &mut State, count: usize) {
    // TODO: use a transaction
    state.selection = state.move_selection(Direction::Forward, Granularity::Line, count);
}

// avoid select by default by having a visual mode switch that makes movements into selects

// insert mode:
// first we calculate the correct cursors/selections
// then we just append at each cursor
// lastly, if it was append mode we shift cursor by 1?

// inserts at the start of each selection
pub fn insert_mode(state: &mut State, _count: usize) {
    state.mode = Mode::Insert;

    state.selection = state
        .selection
        .transform(|range| Range::new(range.to(), range.from()))
}

// inserts at the end of each selection
pub fn append_mode(state: &mut State, _count: usize) {
    state.mode = Mode::Insert;

    // TODO: as transaction
    let text = &state.doc.slice(..);
    state.selection = state.selection.transform(|range| {
        // TODO: to() + next char
        Range::new(
            range.from(),
            graphemes::next_grapheme_boundary(text, range.to()),
        )
    })
}

// TODO: I, A, o and O can share a lot of the primitives.

fn selection_lines(state: &State) -> Vec<usize> {
    // calculate line numbers for each selection range
    let mut lines = state
        .selection
        .ranges()
        .iter()
        .map(|range| state.doc.char_to_line(range.head))
        .collect::<Vec<_>>();

    lines.sort();
    lines.dedup();

    lines
}

// I inserts at the start of each line with a selection
pub fn prepend_to_line(state: &mut State, _count: usize) {
    state.mode = Mode::Insert;

    let lines = selection_lines(state);

    let positions = lines
        .into_iter()
        .map(|index| {
            // adjust all positions to the start of the line.
            state.doc.line_to_char(index)
        })
        .map(|pos| Range::new(pos, pos));

    let selection = Selection::new(positions.collect(), 0);

    let transaction = Transaction::new(state).with_selection(selection);

    transaction.apply(state);
    // TODO: need to store into history if successful
}

// A inserts at the end of each line with a selection
pub fn append_to_line(state: &mut State, _count: usize) {
    state.mode = Mode::Insert;

    let lines = selection_lines(state);

    let positions = lines
        .into_iter()
        .map(|index| {
            // adjust all positions to the end of the line.
            let line = state.doc.line(index);
            let line_start = state.doc.line_to_char(index);
            line_start + line.len_chars() - 1
        })
        .map(|pos| Range::new(pos, pos));

    let selection = Selection::new(positions.collect(), 0);

    let transaction = Transaction::new(state).with_selection(selection);

    transaction.apply(state);
    // TODO: need to store into history if successful
}

// o inserts a new line after each line with a selection
pub fn open_below(state: &mut State, _count: usize) {
    state.mode = Mode::Insert;

    let lines = selection_lines(state);

    let positions: Vec<_> = lines
        .into_iter()
        .map(|index| {
            // adjust all positions to the end of the line.
            let line = state.doc.line(index);
            let line_start = state.doc.line_to_char(index);
            line_start + line.len_chars()
        })
        .collect();

    let changes = positions.iter().copied().map(|index|
        // generate changes
        (index, index, Some(Tendril::from_char('\n'))));

    // TODO: count actually inserts "n" new lines and starts editing on all of them.
    // TODO: append "count" newlines and modify cursors to those lines

    let selection = Selection::new(
        positions
            .iter()
            .copied()
            .map(|pos| Range::new(pos, pos))
            .collect(),
        0,
    );

    let transaction = Transaction::change(state, changes.collect()).with_selection(selection);

    transaction.apply(state);
    // TODO: need to store into history if successful
}

// O inserts a new line before each line with a selection

pub fn normal_mode(state: &mut State, _count: usize) {
    // TODO: if leaving append mode, move cursor back by 1
    state.mode = Mode::Normal;
}

// TODO: insert means add text just before cursor, on exit we should be on the last letter.
pub fn insert_char(state: &mut State, c: char) {
    let c = Tendril::from_char(c);
    let transaction = Transaction::insert(&state, c);

    transaction.apply(state);
    // TODO: need to store into history if successful
}

// TODO: handle indent-aware delete
pub fn delete_char_backward(state: &mut State, count: usize) {
    let text = &state.doc.slice(..);
    let transaction = Transaction::change_by_selection(state, |range| {
        (
            graphemes::nth_prev_grapheme_boundary(text, range.head, count),
            range.head,
            None,
        )
    });
    transaction.apply(state);
    // TODO: need to store into history if successful
}

pub fn delete_char_forward(state: &mut State, count: usize) {
    let text = &state.doc.slice(..);
    let transaction = Transaction::change_by_selection(state, |range| {
        (
            graphemes::nth_next_grapheme_boundary(text, range.head, count),
            range.head,
            None,
        )
    });
    transaction.apply(state);
    // TODO: need to store into history if successful
}