Serenity Operating System
1/*
2 * Copyright (c) 2021-2022, the SerenityOS developers.
3 *
4 * SPDX-License-Identifier: BSD-2-Clause
5 */
6
7#include <AK/CharacterTypes.h>
8#include <LibGUI/EditingEngine.h>
9#include <LibGUI/Event.h>
10#include <LibGUI/TextEditor.h>
11
12namespace GUI {
13
14void EditingEngine::attach(TextEditor& editor)
15{
16 VERIFY(!m_editor);
17 m_editor = editor;
18}
19
20void EditingEngine::detach()
21{
22 VERIFY(m_editor);
23 m_editor = nullptr;
24}
25
26bool EditingEngine::on_key(KeyEvent const& event)
27{
28 if (event.key() == KeyCode::Key_Left) {
29 if (!event.shift() && m_editor->selection().is_valid()) {
30 m_editor->set_cursor(m_editor->selection().normalized().start());
31 m_editor->selection().clear();
32 m_editor->did_update_selection();
33 if (!event.ctrl()) {
34 m_editor->update();
35 return true;
36 }
37 }
38 if (event.ctrl()) {
39 m_editor->update_selection(event.shift());
40 move_to_previous_span();
41 if (event.shift() && m_editor->selection().start().is_valid()) {
42 m_editor->selection().set_end(m_editor->cursor());
43 m_editor->did_update_selection();
44 }
45 return true;
46 }
47 m_editor->update_selection(event.shift());
48 move_one_left();
49 if (event.shift() && m_editor->selection().start().is_valid()) {
50 m_editor->selection().set_end(m_editor->cursor());
51 m_editor->did_update_selection();
52 }
53 return true;
54 }
55
56 if (event.key() == KeyCode::Key_Right) {
57 if (!event.shift() && m_editor->selection().is_valid()) {
58 m_editor->set_cursor(m_editor->selection().normalized().end());
59 m_editor->selection().clear();
60 m_editor->did_update_selection();
61 if (!event.ctrl()) {
62 m_editor->update();
63 return true;
64 }
65 }
66 if (event.ctrl()) {
67 m_editor->update_selection(event.shift());
68 move_to_next_span();
69 if (event.shift() && m_editor->selection().start().is_valid()) {
70 m_editor->selection().set_end(m_editor->cursor());
71 m_editor->did_update_selection();
72 }
73 return true;
74 }
75 m_editor->update_selection(event.shift());
76 move_one_right();
77 if (event.shift() && m_editor->selection().start().is_valid()) {
78 m_editor->selection().set_end(m_editor->cursor());
79 m_editor->did_update_selection();
80 }
81 return true;
82 }
83
84 if (event.key() == KeyCode::Key_Up || event.key() == KeyCode::Key_Down) {
85 auto const direction = key_code_to_vertical_direction(event.key());
86
87 bool const condition_for_up = direction == VerticalDirection::Up && m_editor->cursor().line() > 0;
88 bool const condition_for_down = direction == VerticalDirection::Down && m_editor->cursor().line() < (m_editor->line_count() - 1);
89
90 bool const condition_for_up_to_beginning = direction == VerticalDirection::Up && m_editor->cursor().line() == 0;
91 bool const condition_for_down_to_end = direction == VerticalDirection::Down && m_editor->cursor().line() == (m_editor->line_count() - 1);
92
93 if (condition_for_up || condition_for_down || m_editor->is_wrapping_enabled())
94 m_editor->update_selection(event.shift());
95
96 // Shift + Up on the top line (or only line) selects from the cursor to the start of the line.
97 if (condition_for_up_to_beginning) {
98 m_editor->update_selection(event.shift());
99 move_to_line_beginning();
100 }
101
102 // Shift + Down on the bottom line (or only line) selects from the cursor to the end of the line.
103 if (condition_for_down_to_end) {
104 m_editor->update_selection(event.shift());
105 move_to_line_end();
106 }
107
108 move_one_helper(event, direction);
109 return true;
110 }
111
112 if (event.key() == KeyCode::Key_Home) {
113 m_editor->update_selection(event.shift());
114 if (event.ctrl()) {
115 move_to_first_line();
116 } else {
117 move_to_line_beginning();
118 }
119 if (event.shift() && m_editor->selection().start().is_valid()) {
120 m_editor->selection().set_end(m_editor->cursor());
121 m_editor->did_update_selection();
122 }
123 return true;
124 }
125
126 if (event.key() == KeyCode::Key_End) {
127 m_editor->update_selection(event.shift());
128 if (event.ctrl()) {
129 move_to_last_line();
130 } else {
131 move_to_line_end();
132 }
133 if (event.shift() && m_editor->selection().start().is_valid()) {
134 m_editor->selection().set_end(m_editor->cursor());
135 m_editor->did_update_selection();
136 }
137 return true;
138 }
139
140 if (event.key() == KeyCode::Key_PageUp) {
141 if (m_editor->cursor().line() > 0 || m_editor->is_wrapping_enabled()) {
142 m_editor->update_selection(event.shift());
143 }
144 move_page_up();
145 if (event.shift() && m_editor->selection().start().is_valid()) {
146 m_editor->selection().set_end(m_editor->cursor());
147 m_editor->did_update_selection();
148 }
149 return true;
150 }
151
152 if (event.key() == KeyCode::Key_PageDown) {
153 if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_wrapping_enabled()) {
154 m_editor->update_selection(event.shift());
155 }
156 move_page_down();
157 if (event.shift() && m_editor->selection().start().is_valid()) {
158 m_editor->selection().set_end(m_editor->cursor());
159 m_editor->did_update_selection();
160 }
161 return true;
162 }
163
164 return false;
165}
166
167void EditingEngine::move_one_left()
168{
169 if (m_editor->cursor().column() > 0) {
170 auto new_column = m_editor->document().get_previous_grapheme_cluster_boundary(m_editor->cursor());
171 m_editor->set_cursor(m_editor->cursor().line(), new_column);
172 } else if (m_editor->cursor().line() > 0) {
173 auto new_line = m_editor->cursor().line() - 1;
174 auto new_column = m_editor->lines()[new_line]->length();
175 m_editor->set_cursor(new_line, new_column);
176 }
177}
178
179void EditingEngine::move_one_right()
180{
181 auto new_line = m_editor->cursor().line();
182 auto new_column = m_editor->cursor().column();
183
184 if (m_editor->cursor().column() < m_editor->current_line().length()) {
185 new_line = m_editor->cursor().line();
186 new_column = m_editor->document().get_next_grapheme_cluster_boundary(m_editor->cursor());
187 } else if (m_editor->cursor().line() != m_editor->line_count() - 1) {
188 new_line = m_editor->cursor().line() + 1;
189 new_column = 0;
190 }
191
192 m_editor->set_cursor(new_line, new_column);
193}
194
195void EditingEngine::move_to_previous_span()
196{
197 TextPosition new_cursor;
198 if (m_editor->document().has_spans()) {
199 auto span = m_editor->document().first_non_skippable_span_before(m_editor->cursor());
200 if (span.has_value()) {
201 new_cursor = span.value().range.start();
202 } else {
203 // No remaining spans, just use word break calculation
204 new_cursor = m_editor->document().first_word_break_before(m_editor->cursor(), true);
205 }
206 } else {
207 new_cursor = m_editor->document().first_word_break_before(m_editor->cursor(), true);
208 }
209 m_editor->set_cursor(new_cursor);
210}
211
212void EditingEngine::move_to_next_span()
213{
214 TextPosition new_cursor;
215 if (m_editor->document().has_spans()) {
216 auto span = m_editor->document().first_non_skippable_span_after(m_editor->cursor());
217 if (span.has_value()) {
218 new_cursor = span.value().range.start();
219 } else {
220 // No remaining spans, just use word break calculation
221 new_cursor = m_editor->document().first_word_break_after(m_editor->cursor());
222 }
223 } else {
224 new_cursor = m_editor->document().first_word_break_after(m_editor->cursor());
225 }
226 m_editor->set_cursor(new_cursor);
227}
228
229void EditingEngine::move_to_logical_line_beginning()
230{
231 TextPosition new_cursor;
232 size_t first_nonspace_column = m_editor->current_line().first_non_whitespace_column();
233 if (m_editor->cursor().column() == first_nonspace_column) {
234 new_cursor = { m_editor->cursor().line(), 0 };
235 } else {
236 new_cursor = { m_editor->cursor().line(), first_nonspace_column };
237 }
238 m_editor->set_cursor(new_cursor);
239}
240
241void EditingEngine::move_to_line_beginning()
242{
243 if (m_editor->is_wrapping_enabled()) {
244 TextPosition new_cursor;
245
246 auto home_position = m_editor->cursor_content_rect().location().translated(-m_editor->width(), 0);
247 auto start_of_visual_line = m_editor->text_position_at_content_position(home_position);
248 auto first_non_space_column = m_editor->current_line().first_non_whitespace_column();
249
250 // Subsequent "move_to_line_beginning()" calls move us in the following way:
251 // 1. To the start of the current visual line
252 // 2. To the first non-whitespace character on the logical line
253 // 3. To the first character on the logical line
254 // ...and then repeat 2 and 3.
255 if (m_editor->cursor() == start_of_visual_line) {
256 // Already at 1 so go to 2
257 new_cursor = { m_editor->cursor().line(), first_non_space_column };
258 } else if (m_editor->cursor().column() == first_non_space_column) {
259 // At 2 so go to 3
260 new_cursor = { m_editor->cursor().line(), 0 };
261 } else {
262 // Anything else, so go to 1
263 new_cursor = start_of_visual_line;
264 }
265
266 m_editor->set_cursor(new_cursor);
267 } else {
268 move_to_logical_line_beginning();
269 }
270}
271
272void EditingEngine::move_to_line_end()
273{
274 if (m_editor->is_wrapping_enabled())
275 m_editor->set_cursor_to_end_of_visual_line();
276 else
277 move_to_logical_line_end();
278}
279
280void EditingEngine::move_to_logical_line_end()
281{
282 m_editor->set_cursor({ m_editor->cursor().line(), m_editor->current_line().length() });
283}
284
285void EditingEngine::move_one_helper(KeyEvent const& event, VerticalDirection direction)
286{
287 auto const result = direction == VerticalDirection::Up ? move_one_up(event) : move_one_down(event);
288 if (result != DidMoveALine::Yes && event.shift() && m_editor->selection().start().is_valid()) {
289 m_editor->selection().set_end(m_editor->cursor());
290 m_editor->did_update_selection();
291 }
292}
293
294EditingEngine::DidMoveALine EditingEngine::move_one_up(KeyEvent const& event)
295{
296 if (m_editor->cursor().line() > 0 || m_editor->is_wrapping_enabled()) {
297 if (event.ctrl() && event.shift()) {
298 if (MoveLineUpOrDownCommand::valid_operation(*this, VerticalDirection::Up)) {
299 m_editor->execute<MoveLineUpOrDownCommand>(Badge<EditingEngine> {}, event, *this);
300 return DidMoveALine::Yes;
301 }
302 return DidMoveALine::No;
303 }
304 auto position_above = m_editor->cursor_content_rect().location().translated(0, -m_editor->line_height());
305 TextPosition new_cursor = m_editor->text_position_at_content_position(position_above);
306 m_editor->set_cursor(new_cursor);
307 }
308 return DidMoveALine::No;
309};
310
311EditingEngine::DidMoveALine EditingEngine::move_one_down(KeyEvent const& event)
312{
313 if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_wrapping_enabled()) {
314 if (event.ctrl() && event.shift()) {
315 if (MoveLineUpOrDownCommand::valid_operation(*this, VerticalDirection::Down)) {
316 m_editor->execute<MoveLineUpOrDownCommand>(Badge<EditingEngine> {}, event, *this);
317 return DidMoveALine::Yes;
318 }
319 return DidMoveALine::No;
320 }
321 auto position_below = m_editor->cursor_content_rect().location().translated(0, m_editor->line_height());
322 TextPosition new_cursor = m_editor->text_position_at_content_position(position_below);
323 m_editor->set_cursor(new_cursor);
324 }
325 return DidMoveALine::No;
326};
327
328void EditingEngine::move_up(double page_height_factor)
329{
330 if (m_editor->cursor().line() > 0 || m_editor->is_wrapping_enabled()) {
331 int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor);
332 auto position_above = m_editor->cursor_content_rect().location().translated(0, -pixels);
333 TextPosition new_cursor = m_editor->text_position_at_content_position(position_above);
334 m_editor->set_cursor(new_cursor);
335 }
336};
337
338void EditingEngine::move_down(double page_height_factor)
339{
340 if (m_editor->cursor().line() < (m_editor->line_count() - 1) || m_editor->is_wrapping_enabled()) {
341 int pixels = (int)(m_editor->visible_content_rect().height() * page_height_factor);
342 auto position_below = m_editor->cursor_content_rect().location().translated(0, pixels);
343 TextPosition new_cursor = m_editor->text_position_at_content_position(position_below);
344 m_editor->set_cursor(new_cursor);
345 };
346}
347
348void EditingEngine::move_page_up()
349{
350 move_up(1);
351};
352
353void EditingEngine::move_page_down()
354{
355 move_down(1);
356};
357
358void EditingEngine::move_to_first_line()
359{
360 m_editor->set_cursor(0, 0);
361};
362
363void EditingEngine::move_to_last_line()
364{
365 m_editor->set_cursor(m_editor->line_count() - 1, m_editor->lines()[m_editor->line_count() - 1]->length());
366};
367
368void EditingEngine::get_selection_line_boundaries(Badge<MoveLineUpOrDownCommand>, size_t& first_line, size_t& last_line)
369{
370 get_selection_line_boundaries(first_line, last_line);
371}
372
373void EditingEngine::get_selection_line_boundaries(size_t& first_line, size_t& last_line)
374{
375 auto selection = m_editor->normalized_selection();
376 if (!selection.is_valid()) {
377 first_line = m_editor->cursor().line();
378 last_line = m_editor->cursor().line();
379 return;
380 }
381 first_line = selection.start().line();
382 last_line = selection.end().line();
383 if (first_line != last_line && selection.end().column() == 0)
384 last_line -= 1;
385}
386
387void EditingEngine::delete_char()
388{
389 if (!m_editor->is_editable())
390 return;
391 m_editor->do_delete();
392};
393
394void EditingEngine::delete_line()
395{
396 if (!m_editor->is_editable())
397 return;
398 m_editor->delete_current_line();
399};
400
401MoveLineUpOrDownCommand::MoveLineUpOrDownCommand(TextDocument& document, KeyEvent event, EditingEngine& engine)
402 : TextDocumentUndoCommand(document)
403 , m_event(move(event))
404 , m_direction(key_code_to_vertical_direction(m_event.key()))
405 , m_engine(engine)
406 , m_selection(m_engine.editor().selection())
407 , m_cursor(m_engine.editor().cursor())
408{
409}
410
411void MoveLineUpOrDownCommand::redo()
412{
413 move_lines(m_direction);
414}
415
416void MoveLineUpOrDownCommand::undo()
417{
418 move_lines(!m_direction);
419}
420
421bool MoveLineUpOrDownCommand::merge_with(GUI::Command const&)
422{
423 return false;
424}
425
426DeprecatedString MoveLineUpOrDownCommand::action_text() const
427{
428 return "Move a line";
429}
430
431bool MoveLineUpOrDownCommand::valid_operation(EditingEngine& engine, VerticalDirection direction)
432{
433
434 VERIFY(engine.editor().line_count() != 0);
435
436 auto const& selection = engine.editor().selection().normalized();
437 if (selection.is_valid()) {
438 if ((direction == VerticalDirection::Up && selection.start().line() == 0) || (direction == VerticalDirection::Down && selection.end().line() >= engine.editor().line_count() - 1))
439 return false;
440 } else {
441 size_t first_line;
442 size_t last_line;
443 engine.get_selection_line_boundaries(Badge<MoveLineUpOrDownCommand> {}, first_line, last_line);
444
445 if ((direction == VerticalDirection::Up && first_line == 0) || (direction == VerticalDirection::Down && last_line >= engine.editor().line_count() - 1))
446 return false;
447 }
448 return true;
449}
450
451TextRange MoveLineUpOrDownCommand::retrieve_selection(VerticalDirection direction)
452{
453 if (direction == m_direction)
454 return m_selection;
455
456 auto const offset_selection = [this](auto const offset) {
457 auto tmp = m_selection;
458 tmp.start().set_line(tmp.start().line() + offset);
459 tmp.end().set_line(tmp.end().line() + offset);
460
461 return tmp;
462 };
463
464 if (direction == VerticalDirection::Up)
465 return offset_selection(1);
466 if (direction == VerticalDirection::Down)
467 return offset_selection(-1);
468 VERIFY_NOT_REACHED();
469}
470
471void MoveLineUpOrDownCommand::move_lines(VerticalDirection direction)
472{
473 if (m_event.shift() && m_selection.is_valid()) {
474 m_engine.editor().set_selection(retrieve_selection(direction));
475 m_engine.editor().did_update_selection();
476 }
477
478 if (!m_engine.editor().is_editable())
479 return;
480
481 size_t first_line;
482 size_t last_line;
483 m_engine.get_selection_line_boundaries(Badge<MoveLineUpOrDownCommand> {}, first_line, last_line);
484
485 auto const offset = direction == VerticalDirection::Up ? -1 : 1;
486 auto const insertion_index = direction == VerticalDirection::Up ? last_line : first_line;
487 auto const moved_line_index = offset + (direction != VerticalDirection::Up ? last_line : first_line);
488
489 auto moved_line = m_document.take_line(moved_line_index);
490 m_document.insert_line(insertion_index, move(moved_line));
491
492 m_engine.editor().set_cursor({ m_engine.editor().cursor().line() + offset, m_engine.editor().cursor().column() });
493 if (m_engine.editor().has_selection()) {
494 m_engine.editor().selection().start().set_line(m_engine.editor().selection().start().line() + offset);
495 m_engine.editor().selection().end().set_line(m_engine.editor().selection().end().line() + offset);
496 }
497
498 m_engine.editor().did_change();
499 m_engine.editor().update();
500}
501
502}