ratatui_toolkit/widgets/markdown_widget/widget/methods/
handle_key_event.rs1use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
4
5use crate::widgets::markdown_widget::foundation::events::MarkdownEvent;
6use crate::widgets::markdown_widget::widget::enums::MarkdownWidgetMode;
7use crate::widgets::markdown_widget::widget::MarkdownWidget;
8
9impl<'a> MarkdownWidget<'a> {
10 pub fn handle_key_event(&mut self, key: KeyEvent) -> MarkdownEvent {
26 if self.filter_mode {
28 return self.handle_filter_key(key);
29 }
30
31 if key.code == KeyCode::Esc && self.selection.is_active() {
33 self.selection.exit();
34 self.mode = MarkdownWidgetMode::Normal;
35 self.vim.clear_pending_g();
36 return MarkdownEvent::SelectionEnded;
37 }
38
39 if key.code == KeyCode::Char('y') && self.selection.has_selection() {
41 if let Some(text) = self.selection.get_selected_text() {
42 if !text.is_empty() {
43 if let Ok(mut clipboard) = arboard::Clipboard::new() {
44 if clipboard.set_text(&text).is_ok() {
45 self.selection.exit();
46 self.mode = MarkdownWidgetMode::Normal;
47 self.vim.clear_pending_g();
48 return MarkdownEvent::Copied { text };
49 }
50 }
51 }
52 }
53 }
54
55 if key.code == KeyCode::Char('C')
57 && key.modifiers.contains(KeyModifiers::CONTROL)
58 && key.modifiers.contains(KeyModifiers::SHIFT)
59 {
60 if let Some(text) = self.selection.get_selected_text() {
61 if !text.is_empty() {
62 if let Ok(mut clipboard) = arboard::Clipboard::new() {
63 if clipboard.set_text(&text).is_ok() {
64 self.selection.exit();
65 self.mode = MarkdownWidgetMode::Normal;
66 self.vim.clear_pending_g();
67 return MarkdownEvent::Copied { text };
68 }
69 }
70 }
71 }
72 }
73
74 if key.code == KeyCode::Char('g') {
76 if self.vim.check_pending_gg() {
77 self.scroll.scroll_to_top();
79 return MarkdownEvent::FocusedLine {
80 line: self.scroll.current_line,
81 };
82 }
83 self.vim.set_pending_g();
85 return MarkdownEvent::None;
86 }
87
88 self.vim.clear_pending_g();
90
91 match key.code {
93 KeyCode::Char('/') => {
94 self.filter_mode = true;
95 self.filter = Some(String::new());
96 self.mode = MarkdownWidgetMode::Filter;
97 MarkdownEvent::FilterModeChanged {
98 active: true,
99 filter: String::new(),
100 }
101 }
102 KeyCode::Char('j') | KeyCode::Down => {
103 self.scroll.line_down();
105 MarkdownEvent::FocusedLine {
106 line: self.scroll.current_line,
107 }
108 }
109 KeyCode::Char('k') | KeyCode::Up => {
110 self.scroll.line_up();
112 MarkdownEvent::FocusedLine {
113 line: self.scroll.current_line,
114 }
115 }
116 KeyCode::PageDown => {
117 let old_offset = self.scroll.scroll_offset;
118 self.scroll.scroll_down(self.scroll.viewport_height);
119 MarkdownEvent::Scrolled {
120 offset: self.scroll.scroll_offset,
121 direction: (self.scroll.scroll_offset.saturating_sub(old_offset) as i32),
122 }
123 }
124 KeyCode::PageUp => {
125 let old_offset = self.scroll.scroll_offset;
126 self.scroll.scroll_up(self.scroll.viewport_height);
127 MarkdownEvent::Scrolled {
128 offset: self.scroll.scroll_offset,
129 direction: -(old_offset.saturating_sub(self.scroll.scroll_offset) as i32),
130 }
131 }
132 KeyCode::Home => {
133 self.scroll.scroll_to_top();
134 MarkdownEvent::FocusedLine {
135 line: self.scroll.current_line,
136 }
137 }
138 KeyCode::End | KeyCode::Char('G') => {
139 self.scroll.scroll_to_bottom();
140 MarkdownEvent::FocusedLine {
141 line: self.scroll.current_line,
142 }
143 }
144 _ => MarkdownEvent::None,
145 }
146 }
147
148 fn handle_filter_key(&mut self, key: KeyEvent) -> MarkdownEvent {
158 match key.code {
159 KeyCode::Esc => {
160 let focused_line = self.scroll.current_line;
161 self.filter_mode = false;
163 self.filter = None;
164 self.mode = MarkdownWidgetMode::Normal;
165 self.scroll.filter_mode = false;
167 self.scroll.filter = None;
168 self.cache.render = None;
170 MarkdownEvent::FilterModeExited { line: focused_line }
171 }
172 KeyCode::Enter => {
173 let focused_line = self.scroll.current_line;
174 self.filter_mode = false;
176 self.filter = None;
177 self.mode = MarkdownWidgetMode::Normal;
178 self.scroll.filter_mode = false;
180 self.scroll.filter = None;
181 self.cache.render = None;
183 MarkdownEvent::FilterModeExited { line: focused_line }
184 }
185 KeyCode::Backspace => {
186 if let Some(filter) = &mut self.filter {
187 filter.pop();
188 return MarkdownEvent::FilterModeChanged {
189 active: true,
190 filter: filter.clone(),
191 };
192 }
193 MarkdownEvent::None
194 }
195 KeyCode::Char('j') | KeyCode::Down => {
196 let filter = self.filter.clone().unwrap_or_default();
197 let next_line = self.find_next_filter_match(filter);
198 if let Some(line) = next_line {
199 self.scroll.current_line = line;
200 }
201 MarkdownEvent::FocusedLine {
202 line: self.scroll.current_line,
203 }
204 }
205 KeyCode::Char('k') | KeyCode::Up => {
206 let filter = self.filter.clone().unwrap_or_default();
207 let prev_line = self.find_prev_filter_match(filter);
208 if let Some(line) = prev_line {
209 self.scroll.current_line = line;
210 }
211 MarkdownEvent::FocusedLine {
212 line: self.scroll.current_line,
213 }
214 }
215 KeyCode::Char('n') if key.modifiers.contains(KeyModifiers::CONTROL) => {
216 let filter = self.filter.clone().unwrap_or_default();
217 let next_line = self.find_next_filter_match(filter);
218 if let Some(line) = next_line {
219 self.scroll.current_line = line;
220 }
221 MarkdownEvent::FocusedLine {
222 line: self.scroll.current_line,
223 }
224 }
225 KeyCode::Char('p') if key.modifiers.contains(KeyModifiers::CONTROL) => {
226 let filter = self.filter.clone().unwrap_or_default();
227 let prev_line = self.find_prev_filter_match(filter);
228 if let Some(line) = prev_line {
229 self.scroll.current_line = line;
230 }
231 MarkdownEvent::FocusedLine {
232 line: self.scroll.current_line,
233 }
234 }
235 KeyCode::Char(c) => {
236 if let Some(filter) = &mut self.filter {
237 filter.push(c);
238 return MarkdownEvent::FilterModeChanged {
239 active: true,
240 filter: filter.clone(),
241 };
242 }
243 MarkdownEvent::None
244 }
245 _ => MarkdownEvent::None,
246 }
247 }
248
249 fn find_next_filter_match(&self, filter: String) -> Option<usize> {
251 if filter.is_empty() {
252 return None;
253 }
254 let filter_lower = filter.to_lowercase();
255 let elements =
256 crate::widgets::markdown_widget::foundation::parser::render_markdown_to_elements(
257 self.content,
258 true,
259 );
260 let current = self.scroll.current_line;
261
262 for (idx, element) in elements.iter().enumerate() {
263 let line_num = idx + 1;
264 if line_num <= current {
265 continue;
266 }
267 if !crate::widgets::markdown_widget::extensions::selection::should_render_line(
268 element,
269 idx,
270 self.collapse,
271 ) {
272 continue;
273 }
274 let text = super::super::helpers::element_to_plain_text_for_filter(&element.kind)
275 .to_lowercase();
276 if text.contains(&filter_lower) {
277 return Some(line_num);
278 }
279 }
280 None
281 }
282
283 fn find_prev_filter_match(&self, filter: String) -> Option<usize> {
285 if filter.is_empty() {
286 return None;
287 }
288 let filter_lower = filter.to_lowercase();
289 let elements =
290 crate::widgets::markdown_widget::foundation::parser::render_markdown_to_elements(
291 self.content,
292 true,
293 );
294 let current = self.scroll.current_line;
295
296 for (idx, element) in elements.iter().enumerate().rev() {
297 let line_num = idx + 1;
298 if line_num >= current {
299 continue;
300 }
301 if !crate::widgets::markdown_widget::extensions::selection::should_render_line(
302 element,
303 idx,
304 self.collapse,
305 ) {
306 continue;
307 }
308 let text = super::super::helpers::element_to_plain_text_for_filter(&element.kind)
309 .to_lowercase();
310 if text.contains(&filter_lower) {
311 return Some(line_num);
312 }
313 }
314 None
315 }
316}