Skip to main content

alma/vim/
normal.rs

1//! Normal-mode command application.
2
3use super::{
4    CharSearch, CharSearchDirection, ColumnMotion, Counted, ModeSwitch, Motion, NormalCommand,
5    NormalGrammar, NormalGrammarOutput, SearchOutcome, VimCursor, VimMode, VimSearchState,
6    VimSelectionState, VimStatusLine, VisualMode, motion,
7};
8
9/// Normal-mode controller.
10#[derive(Clone, Debug, Default)]
11pub struct NormalState {
12    /// Grammar.
13    grammar: NormalGrammar,
14    /// Last character-search motion for `;` and `,` repeats.
15    last_char_search: Option<CharSearch>,
16}
17
18impl NormalState {
19    /// Clears pending grammar.
20    pub const fn reset_grammar(&mut self) {
21        self.grammar.reset();
22    }
23
24    /// Feeds and applies one token.
25    pub fn feed(
26        &mut self,
27        token: super::KeyToken,
28        context: NormalCommandContext<'_>,
29    ) -> NormalGrammarOutput {
30        match self.feed_command(token) {
31            NormalGrammarOutput::Command(command) => {
32                self.apply_command(command, context);
33                NormalGrammarOutput::Command(command)
34            }
35            output @ (NormalGrammarOutput::Pending | NormalGrammarOutput::Unmatched) => output,
36        }
37    }
38
39    /// Feeds one token without applying it.
40    pub fn feed_command(&mut self, token: super::KeyToken) -> NormalGrammarOutput {
41        self.grammar.feed(token)
42    }
43
44    /// Applies one command.
45    pub fn apply_command(&mut self, command: NormalCommand, context: NormalCommandContext<'_>) {
46        let NormalCommandContext {
47            text,
48            cursor,
49            mode,
50            selection_state,
51            search_state,
52            command_state,
53            status_line,
54        } = context;
55
56        match command {
57            NormalCommand::Motion(motion) => {
58                self.apply_counted_motion(text, cursor, motion);
59            }
60            NormalCommand::ModeSwitch(ModeSwitch::VisualCharacterwise) => {
61                toggle_visual_mode(
62                    text,
63                    cursor,
64                    mode,
65                    selection_state,
66                    VisualMode::Characterwise,
67                );
68            }
69            NormalCommand::ModeSwitch(ModeSwitch::VisualLinewise) => {
70                toggle_visual_mode(text, cursor, mode, selection_state, VisualMode::Linewise);
71            }
72            NormalCommand::ExCommandStart => {
73                status_line.clear();
74                command_state.start();
75            }
76            NormalCommand::SearchStart(direction) => {
77                status_line.clear();
78                search_state.start(direction);
79            }
80            NormalCommand::SearchRepeat(direction) => {
81                let outcome = search_state.repeat_relative(text, cursor.byte_index(), direction);
82                apply_search_outcome(text, cursor, status_line, outcome);
83            }
84            NormalCommand::ViewportPosition(_) | NormalCommand::Operator { .. } => {}
85        }
86    }
87
88    /// Repeats relative motions; resolves addresses once.
89    pub fn apply_counted_motion(
90        &mut self,
91        text: &str,
92        cursor: &mut VimCursor,
93        counted: Counted<Motion>,
94    ) {
95        match counted.item {
96            Motion::CharSearch(search) => {
97                self.last_char_search = Some(search);
98                for _step in 0..counted.count.get() {
99                    cursor.apply_motion(text, Motion::CharSearch(search));
100                }
101            }
102            Motion::RepeatCharSearch => {
103                if let Some(search) = self.last_char_search {
104                    for _step in 0..counted.count.get() {
105                        cursor.apply_motion(text, Motion::CharSearch(search));
106                    }
107                }
108            }
109            Motion::RepeatCharSearchReversed => {
110                if let Some(search) = self.last_char_search.map(reverse_char_search) {
111                    for _step in 0..counted.count.get() {
112                        cursor.apply_motion(text, Motion::CharSearch(search));
113                    }
114                }
115            }
116            Motion::LineAddress(_) => cursor.apply_motion(text, counted.item),
117            Motion::Column(ColumnMotion::ScreenColumn) => {
118                cursor.set_byte_index(
119                    text,
120                    motion::apply_screen_column_motion(
121                        text,
122                        cursor.byte_index(),
123                        counted.count.get(),
124                    ),
125                );
126            }
127            motion => {
128                for _step in 0..counted.count.get() {
129                    cursor.apply_motion(text, motion);
130                }
131            }
132        }
133    }
134}
135
136/// Normal-command context.
137pub struct NormalCommandContext<'state> {
138    /// Text.
139    pub text: &'state str,
140    /// Cursor state.
141    pub cursor: &'state mut VimCursor,
142    /// Mode.
143    pub mode: &'state mut VimMode,
144    /// Selection.
145    pub selection_state: &'state mut VimSelectionState,
146    /// Search.
147    pub search_state: &'state mut VimSearchState,
148    /// Command-line state.
149    pub command_state: &'state mut super::VimCommandState,
150    /// Status.
151    pub status_line: &'state mut VimStatusLine,
152}
153
154/// Toggles visual mode and anchor state.
155fn toggle_visual_mode(
156    text: &str,
157    cursor: &VimCursor,
158    mode: &mut VimMode,
159    selection_state: &mut VimSelectionState,
160    visual_mode: VisualMode,
161) {
162    if *mode == VimMode::Visual(visual_mode) {
163        *mode = VimMode::Normal;
164        selection_state.clear();
165    } else {
166        *mode = VimMode::Visual(visual_mode);
167        selection_state.start(text, cursor.byte_index());
168    }
169}
170
171/// Inverts `;` for `,`.
172const fn reverse_char_search(search: CharSearch) -> CharSearch {
173    let direction = match search.direction {
174        CharSearchDirection::Backward => CharSearchDirection::Forward,
175        CharSearchDirection::Forward => CharSearchDirection::Backward,
176    };
177
178    CharSearch {
179        direction,
180        ..search
181    }
182}
183
184/// Applies a search outcome.
185pub fn apply_search_outcome(
186    text: &str,
187    cursor: &mut VimCursor,
188    status_line: &mut VimStatusLine,
189    outcome: SearchOutcome,
190) {
191    match outcome {
192        SearchOutcome::Match { byte_index } => {
193            cursor.set_byte_index(text, byte_index);
194            status_line.clear();
195        }
196        SearchOutcome::Error(error) => status_line.set_error(error),
197    }
198}