Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * All rights reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions are met:
7 *
8 * 1. Redistributions of source code must retain the above copyright notice, this
9 * list of conditions and the following disclaimer.
10 *
11 * 2. Redistributions in binary form must reproduce the above copyright notice,
12 * this list of conditions and the following disclaimer in the documentation
13 * and/or other materials provided with the distribution.
14 *
15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27#include <AK/QuickSort.h>
28#include <AK/StringBuilder.h>
29#include <Kernel/KeyCode.h>
30#include <LibGUI/Action.h>
31#include <LibGUI/Clipboard.h>
32#include <LibGUI/FontDatabase.h>
33#include <LibGUI/InputBox.h>
34#include <LibGUI/Menu.h>
35#include <LibGUI/Painter.h>
36#include <LibGUI/ScrollBar.h>
37#include <LibGUI/SyntaxHighlighter.h>
38#include <LibGUI/TextEditor.h>
39#include <LibGUI/Window.h>
40#include <LibGfx/Bitmap.h>
41#include <LibGfx/Font.h>
42#include <LibGfx/Palette.h>
43#include <ctype.h>
44#include <fcntl.h>
45#include <stdio.h>
46#include <unistd.h>
47
48//#define DEBUG_GTEXTEDITOR
49
50namespace GUI {
51
52TextEditor::TextEditor(Type type)
53 : m_type(type)
54{
55 set_background_role(ColorRole::Base);
56 set_foreground_role(ColorRole::BaseText);
57 set_document(TextDocument::create());
58 set_scrollbars_enabled(is_multi_line());
59 set_font(GFontDatabase::the().get_by_name("Csilla Thin"));
60 // FIXME: Recompute vertical scrollbar step size on font change.
61 vertical_scrollbar().set_step(line_height());
62 m_cursor = { 0, 0 };
63 create_actions();
64}
65
66TextEditor::~TextEditor()
67{
68 if (m_document)
69 m_document->unregister_client(*this);
70}
71
72void TextEditor::create_actions()
73{
74 m_undo_action = CommonActions::make_undo_action([&](auto&) { undo(); }, this);
75 m_redo_action = CommonActions::make_redo_action([&](auto&) { redo(); }, this);
76 m_undo_action->set_enabled(false);
77 m_redo_action->set_enabled(false);
78 m_cut_action = CommonActions::make_cut_action([&](auto&) { cut(); }, this);
79 m_copy_action = CommonActions::make_copy_action([&](auto&) { copy(); }, this);
80 m_paste_action = CommonActions::make_paste_action([&](auto&) { paste(); }, this);
81 m_delete_action = CommonActions::make_delete_action([&](auto&) { do_delete(); }, this);
82 if (is_multi_line()) {
83 m_go_to_line_action = Action::create(
84 "Go to line...", { Mod_Ctrl, Key_L }, Gfx::Bitmap::load_from_file("/res/icons/16x16/go-forward.png"), [this](auto&) {
85 auto input_box = InputBox::construct("Line:", "Go to line", window());
86 auto result = input_box->exec();
87 if (result == InputBox::ExecOK) {
88 bool ok;
89 auto line_number = input_box->text_value().to_uint(ok);
90 if (ok)
91 set_cursor(line_number - 1, 0);
92 }
93 },
94 this);
95 }
96}
97
98void TextEditor::set_text(const StringView& text)
99{
100 if (is_single_line() && text.length() == line(0).length() && !memcmp(text.characters_without_null_termination(), line(0).characters(), text.length()))
101 return;
102
103 m_selection.clear();
104
105 document().set_text(text);
106
107 update_content_size();
108 recompute_all_visual_lines();
109 if (is_single_line())
110 set_cursor(0, line(0).length());
111 else
112 set_cursor(0, 0);
113 did_update_selection();
114 update();
115}
116
117void TextEditor::update_content_size()
118{
119 int content_width = 0;
120 int content_height = 0;
121 for (auto& line : m_line_visual_data) {
122 content_width = max(line.visual_rect.width(), content_width);
123 content_height += line.visual_rect.height();
124 }
125 content_width += m_horizontal_content_padding * 2;
126 if (is_right_text_alignment(m_text_alignment))
127 content_width = max(frame_inner_rect().width(), content_width);
128
129 set_content_size({ content_width, content_height });
130 set_size_occupied_by_fixed_elements({ ruler_width(), 0 });
131}
132
133TextPosition TextEditor::text_position_at(const Gfx::Point& a_position) const
134{
135 auto position = a_position;
136 position.move_by(horizontal_scrollbar().value(), vertical_scrollbar().value());
137 position.move_by(-(m_horizontal_content_padding + ruler_width()), 0);
138 position.move_by(-frame_thickness(), -frame_thickness());
139
140 size_t line_index = 0;
141
142 if (is_line_wrapping_enabled()) {
143 for (size_t i = 0; i < line_count(); ++i) {
144 auto& rect = m_line_visual_data[i].visual_rect;
145 if (position.y() >= rect.top() && position.y() <= rect.bottom()) {
146 line_index = i;
147 break;
148 }
149 if (position.y() > rect.bottom())
150 line_index = line_count() - 1;
151 }
152 } else {
153 line_index = (size_t)(position.y() / line_height());
154 }
155
156 line_index = max((size_t)0, min(line_index, line_count() - 1));
157
158 size_t column_index;
159 switch (m_text_alignment) {
160 case Gfx::TextAlignment::CenterLeft:
161 column_index = (position.x() + glyph_width() / 2) / glyph_width();
162 if (is_line_wrapping_enabled()) {
163 for_each_visual_line(line_index, [&](const Gfx::Rect& rect, const StringView&, size_t start_of_line) {
164 if (rect.contains_vertically(position.y())) {
165 column_index += start_of_line;
166 return IterationDecision::Break;
167 }
168 return IterationDecision::Continue;
169 });
170 }
171 break;
172 case Gfx::TextAlignment::CenterRight:
173 // FIXME: Support right-aligned line wrapping, I guess.
174 ASSERT(!is_line_wrapping_enabled());
175 column_index = (position.x() - content_x_for_position({ line_index, 0 }) + glyph_width() / 2) / glyph_width();
176 break;
177 default:
178 ASSERT_NOT_REACHED();
179 }
180
181 column_index = max((size_t)0, min(column_index, line(line_index).length()));
182 return { line_index, column_index };
183}
184
185void TextEditor::doubleclick_event(MouseEvent& event)
186{
187 if (event.button() != MouseButton::Left)
188 return;
189
190 // NOTE: This ensures that spans are updated before we look at them.
191 flush_pending_change_notification_if_needed();
192
193 m_triple_click_timer.start();
194 m_in_drag_select = false;
195
196 auto start = text_position_at(event.position());
197 auto end = start;
198 auto& line = this->line(start.line());
199
200 if (!document().has_spans()) {
201 while (start.column() > 0) {
202 if (isspace(line.characters()[start.column() - 1]))
203 break;
204 start.set_column(start.column() - 1);
205 }
206
207 while (end.column() < line.length()) {
208 if (isspace(line.characters()[end.column()]))
209 break;
210 end.set_column(end.column() + 1);
211 }
212 } else {
213 for (auto& span : document().spans()) {
214 if (!span.range.contains(start))
215 continue;
216 start = span.range.start();
217 end = span.range.end();
218 end.set_column(end.column() + 1);
219 break;
220 }
221 }
222
223 m_selection.set(start, end);
224 set_cursor(end);
225 update();
226 did_update_selection();
227}
228
229void TextEditor::mousedown_event(MouseEvent& event)
230{
231 if (event.button() != MouseButton::Left) {
232 return;
233 }
234
235 if (m_triple_click_timer.is_valid() && m_triple_click_timer.elapsed() < 250) {
236 m_triple_click_timer = Core::ElapsedTimer();
237
238 TextPosition start;
239 TextPosition end;
240
241 if (is_multi_line()) {
242 // select *current* line
243 start = TextPosition(m_cursor.line(), 0);
244 end = TextPosition(m_cursor.line(), line(m_cursor.line()).length());
245 } else {
246 // select *whole* line
247 start = TextPosition(0, 0);
248 end = TextPosition(line_count() - 1, line(line_count() - 1).length());
249 }
250
251 m_selection.set(start, end);
252 set_cursor(end);
253 return;
254 }
255
256 if (event.modifiers() & Mod_Shift) {
257 if (!has_selection())
258 m_selection.set(m_cursor, {});
259 } else {
260 m_selection.clear();
261 }
262
263 m_in_drag_select = true;
264
265 set_cursor(text_position_at(event.position()));
266
267 if (!(event.modifiers() & Mod_Shift)) {
268 if (!has_selection())
269 m_selection.set(m_cursor, {});
270 }
271
272 if (m_selection.start().is_valid() && m_selection.start() != m_cursor)
273 m_selection.set_end(m_cursor);
274
275 // FIXME: Only update the relevant rects.
276 update();
277 did_update_selection();
278}
279
280void TextEditor::mouseup_event(MouseEvent& event)
281{
282 if (event.button() == MouseButton::Left) {
283 if (m_in_drag_select) {
284 m_in_drag_select = false;
285 }
286 return;
287 }
288}
289
290void TextEditor::mousemove_event(MouseEvent& event)
291{
292 if (m_in_drag_select) {
293 set_cursor(text_position_at(event.position()));
294 m_selection.set_end(m_cursor);
295 did_update_selection();
296 update();
297 return;
298 }
299}
300
301int TextEditor::ruler_width() const
302{
303 if (!m_ruler_visible)
304 return 0;
305 // FIXME: Resize based on needed space.
306 return 5 * font().glyph_width('x') + 4;
307}
308
309Gfx::Rect TextEditor::ruler_content_rect(size_t line_index) const
310{
311 if (!m_ruler_visible)
312 return {};
313 return {
314 0 - ruler_width() + horizontal_scrollbar().value(),
315 line_content_rect(line_index).y(),
316 ruler_width(),
317 line_content_rect(line_index).height()
318 };
319}
320
321Gfx::Rect TextEditor::ruler_rect_in_inner_coordinates() const
322{
323 return { 0, 0, ruler_width(), height() - height_occupied_by_horizontal_scrollbar() };
324}
325
326Gfx::Rect TextEditor::visible_text_rect_in_inner_coordinates() const
327{
328 return {
329 m_horizontal_content_padding + (m_ruler_visible ? (ruler_rect_in_inner_coordinates().right() + 1) : 0),
330 0,
331 frame_inner_rect().width() - (m_horizontal_content_padding * 2) - width_occupied_by_vertical_scrollbar() - ruler_width(),
332 frame_inner_rect().height() - height_occupied_by_horizontal_scrollbar()
333 };
334}
335
336void TextEditor::paint_event(PaintEvent& event)
337{
338 Color widget_background_color = palette().color(background_role());
339 // NOTE: This ensures that spans are updated before we look at them.
340 flush_pending_change_notification_if_needed();
341
342 Frame::paint_event(event);
343
344 Painter painter(*this);
345 painter.add_clip_rect(widget_inner_rect());
346 painter.add_clip_rect(event.rect());
347 painter.fill_rect(event.rect(), widget_background_color);
348
349 painter.translate(frame_thickness(), frame_thickness());
350
351 auto ruler_rect = ruler_rect_in_inner_coordinates();
352
353 if (m_ruler_visible) {
354 painter.fill_rect(ruler_rect, palette().ruler());
355 painter.draw_line(ruler_rect.top_right(), ruler_rect.bottom_right(), palette().ruler_border());
356 }
357
358 painter.translate(-horizontal_scrollbar().value(), -vertical_scrollbar().value());
359 if (m_ruler_visible)
360 painter.translate(ruler_width(), 0);
361
362 size_t first_visible_line = text_position_at(event.rect().top_left()).line();
363 size_t last_visible_line = text_position_at(event.rect().bottom_right()).line();
364
365 auto selection = normalized_selection();
366 bool has_selection = selection.is_valid();
367
368 if (m_ruler_visible) {
369 for (size_t i = first_visible_line; i <= last_visible_line; ++i) {
370 bool is_current_line = i == m_cursor.line();
371 auto ruler_line_rect = ruler_content_rect(i);
372 painter.draw_text(
373 ruler_line_rect.shrunken(2, 0).translated(0, m_line_spacing / 2),
374 String::number(i + 1),
375 is_current_line ? Gfx::Font::default_bold_font() : font(),
376 Gfx::TextAlignment::TopRight,
377 is_current_line ? palette().ruler_active_text() : palette().ruler_inactive_text());
378 }
379 }
380
381 Gfx::Rect text_clip_rect {
382 (m_ruler_visible ? (ruler_rect_in_inner_coordinates().right() + frame_thickness() + 1) : frame_thickness()),
383 frame_thickness(),
384 width() - width_occupied_by_vertical_scrollbar() - ruler_width(),
385 height() - height_occupied_by_horizontal_scrollbar()
386 };
387 painter.add_clip_rect(text_clip_rect);
388
389 for (size_t line_index = first_visible_line; line_index <= last_visible_line; ++line_index) {
390 auto& line = this->line(line_index);
391
392 bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line();
393 size_t first_visual_line_with_selection = 0;
394 size_t last_visual_line_with_selection = 0;
395 if (physical_line_has_selection) {
396 if (selection.start().line() < line_index)
397 first_visual_line_with_selection = 0;
398 else
399 first_visual_line_with_selection = visual_line_containing(line_index, selection.start().column());
400
401 if (selection.end().line() > line_index)
402 last_visual_line_with_selection = m_line_visual_data[line_index].visual_line_breaks.size();
403 else
404 last_visual_line_with_selection = visual_line_containing(line_index, selection.end().column());
405 }
406
407 size_t selection_start_column_within_line = selection.start().line() == line_index ? selection.start().column() : 0;
408 size_t selection_end_column_within_line = selection.end().line() == line_index ? selection.end().column() : line.length();
409
410 size_t visual_line_index = 0;
411 for_each_visual_line(line_index, [&](const Gfx::Rect& visual_line_rect, const StringView& visual_line_text, size_t start_of_visual_line) {
412 if (is_multi_line() && line_index == m_cursor.line())
413 painter.fill_rect(visual_line_rect, widget_background_color.darkened(0.9f));
414#ifdef DEBUG_GTEXTEDITOR
415 painter.draw_rect(visual_line_rect, Color::Cyan);
416#endif
417 if (!document().has_spans()) {
418 // Fast-path for plain text
419 painter.draw_text(visual_line_rect, visual_line_text, m_text_alignment, palette().color(foreground_role()));
420 } else {
421 int advance = font().glyph_width(' ') + font().glyph_spacing();
422 Gfx::Rect character_rect = { visual_line_rect.location(), { font().glyph_width(' '), line_height() } };
423 for (size_t i = 0; i < visual_line_text.length(); ++i) {
424 const Gfx::Font* font = &this->font();
425 Color color;
426 Optional<Color> background_color;
427 TextPosition physical_position(line_index, start_of_visual_line + i);
428 // FIXME: This is *horribly* inefficient.
429 for (auto& span : document().spans()) {
430 if (!span.range.contains(physical_position))
431 continue;
432 color = span.color;
433 if (span.font)
434 font = span.font;
435 background_color = span.background_color;
436 break;
437 }
438 if (background_color.has_value())
439 painter.fill_rect(character_rect, background_color.value());
440 painter.draw_text(character_rect, visual_line_text.substring_view(i, 1), *font, m_text_alignment, color);
441 character_rect.move_by(advance, 0);
442 }
443 }
444 bool physical_line_has_selection = has_selection && line_index >= selection.start().line() && line_index <= selection.end().line();
445 if (physical_line_has_selection) {
446
447 bool current_visual_line_has_selection = (line_index != selection.start().line() && line_index != selection.end().line())
448 || (visual_line_index >= first_visual_line_with_selection && visual_line_index <= last_visual_line_with_selection);
449 if (current_visual_line_has_selection) {
450 bool selection_begins_on_current_visual_line = visual_line_index == first_visual_line_with_selection;
451 bool selection_ends_on_current_visual_line = visual_line_index == last_visual_line_with_selection;
452
453 int selection_left = selection_begins_on_current_visual_line
454 ? content_x_for_position({ line_index, (size_t)selection_start_column_within_line })
455 : m_horizontal_content_padding;
456
457 int selection_right = selection_ends_on_current_visual_line
458 ? content_x_for_position({ line_index, (size_t)selection_end_column_within_line })
459 : visual_line_rect.right() + 1;
460
461 Gfx::Rect selection_rect {
462 selection_left,
463 visual_line_rect.y(),
464 selection_right - selection_left,
465 visual_line_rect.height()
466 };
467
468 Color background_color = is_focused() ? palette().selection() : palette().inactive_selection();
469 Color text_color = is_focused() ? palette().selection_text() : palette().inactive_selection_text();
470
471 painter.fill_rect(selection_rect, background_color);
472
473 size_t start_of_selection_within_visual_line = (size_t)max(0, (int)selection_start_column_within_line - (int)start_of_visual_line);
474 size_t end_of_selection_within_visual_line = selection_end_column_within_line - start_of_visual_line;
475
476 StringView visual_selected_text {
477 visual_line_text.characters_without_null_termination() + start_of_selection_within_visual_line,
478 end_of_selection_within_visual_line - start_of_selection_within_visual_line
479 };
480
481 painter.draw_text(selection_rect, visual_selected_text, Gfx::TextAlignment::CenterLeft, text_color);
482 }
483 }
484 ++visual_line_index;
485 return IterationDecision::Continue;
486 });
487 }
488
489 if (is_focused() && m_cursor_state)
490 painter.fill_rect(cursor_content_rect(), palette().text_cursor());
491}
492
493void TextEditor::toggle_selection_if_needed_for_event(const KeyEvent& event)
494{
495 if (event.shift() && !m_selection.is_valid()) {
496 m_selection.set(m_cursor, {});
497 did_update_selection();
498 update();
499 return;
500 }
501 if (!event.shift() && m_selection.is_valid()) {
502 m_selection.clear();
503 did_update_selection();
504 update();
505 return;
506 }
507}
508
509void TextEditor::select_all()
510{
511 TextPosition start_of_document { 0, 0 };
512 TextPosition end_of_document { line_count() - 1, line(line_count() - 1).length() };
513 m_selection.set(start_of_document, end_of_document);
514 did_update_selection();
515 set_cursor(end_of_document);
516 update();
517}
518
519void TextEditor::get_selection_line_boundaries(size_t& first_line, size_t& last_line)
520{
521 auto selection = normalized_selection();
522 if (!selection.is_valid()) {
523 first_line = m_cursor.line();
524 last_line = m_cursor.line();
525 return;
526 }
527 first_line = selection.start().line();
528 last_line = selection.end().line();
529 if (first_line != last_line && selection.end().column() == 0)
530 last_line -= 1;
531}
532
533void TextEditor::move_selected_lines_up()
534{
535 size_t first_line;
536 size_t last_line;
537 get_selection_line_boundaries(first_line, last_line);
538
539 if (first_line == 0)
540 return;
541
542 auto& lines = document().lines();
543 lines.insert((int)last_line, lines.take((int)first_line - 1));
544 m_cursor = { first_line - 1, 0 };
545
546 if (has_selection()) {
547 m_selection.set_start({ first_line - 1, 0 });
548 m_selection.set_end({ last_line - 1, line(last_line - 1).length() });
549 }
550
551 did_change();
552 update();
553}
554
555void TextEditor::move_selected_lines_down()
556{
557 size_t first_line;
558 size_t last_line;
559 get_selection_line_boundaries(first_line, last_line);
560
561 auto& lines = document().lines();
562 if (last_line >= (size_t)(lines.size() - 1))
563 return;
564
565 lines.insert((int)first_line, lines.take((int)last_line + 1));
566 m_cursor = { first_line + 1, 0 };
567
568 if (has_selection()) {
569 m_selection.set_start({ first_line + 1, 0 });
570 m_selection.set_end({ last_line + 1, line(last_line + 1).length() });
571 }
572
573 did_change();
574 update();
575}
576
577void TextEditor::sort_selected_lines()
578{
579 if (is_readonly())
580 return;
581
582 if (!has_selection())
583 return;
584
585 size_t first_line;
586 size_t last_line;
587 get_selection_line_boundaries(first_line, last_line);
588
589 auto& lines = document().lines();
590
591 auto start = lines.begin() + (int)first_line;
592 auto end = lines.begin() + (int)last_line + 1;
593
594 quick_sort(start, end, [](auto& a, auto& b) {
595 return strcmp(a.characters(), b.characters()) < 0;
596 });
597
598 did_change();
599 update();
600}
601
602void TextEditor::keydown_event(KeyEvent& event)
603{
604 if (is_single_line() && event.key() == KeyCode::Key_Tab)
605 return Widget::keydown_event(event);
606
607 if (is_single_line() && event.key() == KeyCode::Key_Return) {
608 if (on_return_pressed)
609 on_return_pressed();
610 return;
611 }
612
613 if (event.key() == KeyCode::Key_Escape) {
614 if (on_escape_pressed)
615 on_escape_pressed();
616 return;
617 }
618 if (is_multi_line() && event.key() == KeyCode::Key_Up) {
619 if (m_cursor.line() > 0) {
620 if (event.ctrl() && event.shift()) {
621 move_selected_lines_up();
622 return;
623 }
624 size_t new_line = m_cursor.line() - 1;
625 size_t new_column = min(m_cursor.column(), line(new_line).length());
626 toggle_selection_if_needed_for_event(event);
627 set_cursor(new_line, new_column);
628 if (event.shift() && m_selection.start().is_valid()) {
629 m_selection.set_end(m_cursor);
630 did_update_selection();
631 }
632 }
633 return;
634 }
635 if (is_multi_line() && event.key() == KeyCode::Key_Down) {
636 if (m_cursor.line() < (line_count() - 1)) {
637 if (event.ctrl() && event.shift()) {
638 move_selected_lines_down();
639 return;
640 }
641 size_t new_line = m_cursor.line() + 1;
642 size_t new_column = min(m_cursor.column(), line(new_line).length());
643 toggle_selection_if_needed_for_event(event);
644 set_cursor(new_line, new_column);
645 if (event.shift() && m_selection.start().is_valid()) {
646 m_selection.set_end(m_cursor);
647 did_update_selection();
648 }
649 }
650 return;
651 }
652 if (is_multi_line() && event.key() == KeyCode::Key_PageUp) {
653 if (m_cursor.line() > 0) {
654 size_t page_step = (size_t)visible_content_rect().height() / (size_t)line_height();
655 size_t new_line = m_cursor.line() < page_step ? 0 : m_cursor.line() - page_step;
656 size_t new_column = min(m_cursor.column(), line(new_line).length());
657 toggle_selection_if_needed_for_event(event);
658 set_cursor(new_line, new_column);
659 if (event.shift() && m_selection.start().is_valid()) {
660 m_selection.set_end(m_cursor);
661 did_update_selection();
662 }
663 }
664 return;
665 }
666 if (is_multi_line() && event.key() == KeyCode::Key_PageDown) {
667 if (m_cursor.line() < (line_count() - 1)) {
668 int new_line = min(line_count() - 1, m_cursor.line() + visible_content_rect().height() / line_height());
669 int new_column = min(m_cursor.column(), lines()[new_line].length());
670 toggle_selection_if_needed_for_event(event);
671 set_cursor(new_line, new_column);
672 if (event.shift() && m_selection.start().is_valid()) {
673 m_selection.set_end(m_cursor);
674 did_update_selection();
675 }
676 }
677 return;
678 }
679 if (event.key() == KeyCode::Key_Left) {
680 if (event.ctrl() && document().has_spans()) {
681 // FIXME: Do something nice when the document has no spans.
682 auto span = document().first_non_skippable_span_before(m_cursor);
683 TextPosition new_cursor = !span.has_value()
684 ? TextPosition(0, 0)
685 : span.value().range.start();
686 toggle_selection_if_needed_for_event(event);
687 set_cursor(new_cursor);
688 if (event.shift() && m_selection.start().is_valid()) {
689 m_selection.set_end(m_cursor);
690 did_update_selection();
691 }
692 return;
693 }
694 if (m_cursor.column() > 0) {
695 int new_column = m_cursor.column() - 1;
696 toggle_selection_if_needed_for_event(event);
697 set_cursor(m_cursor.line(), new_column);
698 if (event.shift() && m_selection.start().is_valid()) {
699 m_selection.set_end(m_cursor);
700 did_update_selection();
701 }
702 } else if (m_cursor.line() > 0) {
703 int new_line = m_cursor.line() - 1;
704 int new_column = lines()[new_line].length();
705 toggle_selection_if_needed_for_event(event);
706 set_cursor(new_line, new_column);
707 if (event.shift() && m_selection.start().is_valid()) {
708 m_selection.set_end(m_cursor);
709 did_update_selection();
710 }
711 }
712 return;
713 }
714 if (event.key() == KeyCode::Key_Right) {
715 if (event.ctrl() && document().has_spans()) {
716 // FIXME: Do something nice when the document has no spans.
717 auto span = document().first_non_skippable_span_after(m_cursor);
718 TextPosition new_cursor = !span.has_value()
719 ? document().spans().last().range.end()
720 : span.value().range.start();
721 toggle_selection_if_needed_for_event(event);
722 set_cursor(new_cursor);
723 if (event.shift() && m_selection.start().is_valid()) {
724 m_selection.set_end(m_cursor);
725 did_update_selection();
726 }
727 return;
728 }
729 int new_line = m_cursor.line();
730 int new_column = m_cursor.column();
731 if (m_cursor.column() < current_line().length()) {
732 new_line = m_cursor.line();
733 new_column = m_cursor.column() + 1;
734 } else if (m_cursor.line() != line_count() - 1) {
735 new_line = m_cursor.line() + 1;
736 new_column = 0;
737 }
738 toggle_selection_if_needed_for_event(event);
739 set_cursor(new_line, new_column);
740 if (event.shift() && m_selection.start().is_valid()) {
741 m_selection.set_end(m_cursor);
742 did_update_selection();
743 }
744 return;
745 }
746 if (!event.ctrl() && event.key() == KeyCode::Key_Home) {
747 size_t first_nonspace_column = current_line().first_non_whitespace_column();
748 toggle_selection_if_needed_for_event(event);
749 if (m_cursor.column() == first_nonspace_column)
750 set_cursor(m_cursor.line(), 0);
751 else
752 set_cursor(m_cursor.line(), first_nonspace_column);
753 if (event.shift() && m_selection.start().is_valid()) {
754 m_selection.set_end(m_cursor);
755 did_update_selection();
756 }
757 return;
758 }
759 if (!event.ctrl() && event.key() == KeyCode::Key_End) {
760 toggle_selection_if_needed_for_event(event);
761 set_cursor(m_cursor.line(), current_line().length());
762 if (event.shift() && m_selection.start().is_valid()) {
763 m_selection.set_end(m_cursor);
764 did_update_selection();
765 }
766 return;
767 }
768 if (event.ctrl() && event.key() == KeyCode::Key_Home) {
769 toggle_selection_if_needed_for_event(event);
770 set_cursor(0, 0);
771 if (event.shift() && m_selection.start().is_valid()) {
772 m_selection.set_end(m_cursor);
773 did_update_selection();
774 }
775 return;
776 }
777 if (event.ctrl() && event.key() == KeyCode::Key_End) {
778 toggle_selection_if_needed_for_event(event);
779 set_cursor(line_count() - 1, lines()[line_count() - 1].length());
780 if (event.shift() && m_selection.start().is_valid()) {
781 m_selection.set_end(m_cursor);
782 did_update_selection();
783 }
784 return;
785 }
786 if (event.modifiers() == Mod_Ctrl && event.key() == KeyCode::Key_A) {
787 select_all();
788 return;
789 }
790 if (event.alt() && event.shift() && event.key() == KeyCode::Key_S) {
791 sort_selected_lines();
792 return;
793 }
794 if (event.key() == KeyCode::Key_Backspace) {
795 if (is_readonly())
796 return;
797 if (has_selection()) {
798 delete_selection();
799 did_update_selection();
800 return;
801 }
802 if (m_cursor.column() > 0) {
803 int erase_count = 1;
804 if (current_line().first_non_whitespace_column() >= m_cursor.column()) {
805 int new_column;
806 if (m_cursor.column() % m_soft_tab_width == 0)
807 new_column = m_cursor.column() - m_soft_tab_width;
808 else
809 new_column = (m_cursor.column() / m_soft_tab_width) * m_soft_tab_width;
810 erase_count = m_cursor.column() - new_column;
811 }
812
813 // Backspace within line
814 TextRange erased_range({ m_cursor.line(), m_cursor.column() - erase_count }, m_cursor);
815 auto erased_text = document().text_in_range(erased_range);
816 execute<RemoveTextCommand>(erased_text, erased_range);
817 return;
818 }
819 if (m_cursor.column() == 0 && m_cursor.line() != 0) {
820 // Backspace at column 0; merge with previous line
821 size_t previous_length = line(m_cursor.line() - 1).length();
822 TextRange erased_range({ m_cursor.line() - 1, previous_length }, m_cursor);
823 execute<RemoveTextCommand>("\n", erased_range);
824 return;
825 }
826 return;
827 }
828
829 if (event.modifiers() == Mod_Shift && event.key() == KeyCode::Key_Delete) {
830 if (is_readonly())
831 return;
832 delete_current_line();
833 return;
834 }
835
836 if (event.key() == KeyCode::Key_Delete) {
837 if (is_readonly())
838 return;
839 do_delete();
840 return;
841 }
842
843 if (!is_readonly() && !event.ctrl() && !event.alt() && !event.text().is_empty()) {
844 insert_at_cursor_or_replace_selection(event.text());
845 return;
846 }
847
848 event.ignore();
849}
850
851void TextEditor::delete_current_line()
852{
853 if (has_selection())
854 return delete_selection();
855
856 TextPosition start;
857 TextPosition end;
858 if (m_cursor.line() == 0 && line_count() == 1) {
859 start = { 0, 0 };
860 end = { 0, line(0).length() };
861 } else if (m_cursor.line() == line_count() - 1) {
862 start = { m_cursor.line() - 1, line(m_cursor.line()).length() };
863 end = { m_cursor.line(), line(m_cursor.line()).length() };
864 } else {
865 start = { m_cursor.line(), 0 };
866 end = { m_cursor.line() + 1, 0 };
867 }
868
869 TextRange erased_range(start, end);
870 execute<RemoveTextCommand>(document().text_in_range(erased_range), erased_range);
871}
872
873void TextEditor::do_delete()
874{
875 if (is_readonly())
876 return;
877
878 if (has_selection())
879 return delete_selection();
880
881 if (m_cursor.column() < current_line().length()) {
882 // Delete within line
883 TextRange erased_range(m_cursor, { m_cursor.line(), m_cursor.column() + 1 });
884 execute<RemoveTextCommand>(document().text_in_range(erased_range), erased_range);
885 return;
886 }
887 if (m_cursor.column() == current_line().length() && m_cursor.line() != line_count() - 1) {
888 // Delete at end of line; merge with next line
889 TextRange erased_range(m_cursor, { m_cursor.line() + 1, 0 });
890 execute<RemoveTextCommand>(document().text_in_range(erased_range), erased_range);
891 return;
892 }
893}
894
895int TextEditor::content_x_for_position(const TextPosition& position) const
896{
897 auto& line = this->line(position.line());
898 int x_offset = -1;
899 switch (m_text_alignment) {
900 case Gfx::TextAlignment::CenterLeft:
901 for_each_visual_line(position.line(), [&](const Gfx::Rect&, const StringView& view, size_t start_of_visual_line) {
902 if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
903 x_offset = (position.column() - start_of_visual_line) * glyph_width();
904 return IterationDecision::Break;
905 }
906 return IterationDecision::Continue;
907 });
908 return m_horizontal_content_padding + x_offset;
909 case Gfx::TextAlignment::CenterRight:
910 // FIXME
911 ASSERT(!is_line_wrapping_enabled());
912 return content_width() - m_horizontal_content_padding - (line.length() * glyph_width()) + (position.column() * glyph_width());
913 default:
914 ASSERT_NOT_REACHED();
915 }
916}
917
918Gfx::Rect TextEditor::content_rect_for_position(const TextPosition& position) const
919{
920 if (!position.is_valid())
921 return {};
922 ASSERT(!lines().is_empty());
923 ASSERT(position.column() <= (current_line().length() + 1));
924
925 int x = content_x_for_position(position);
926
927 if (is_single_line()) {
928 Gfx::Rect rect { x, 0, 1, font().glyph_height() + 2 };
929 rect.center_vertically_within({ {}, frame_inner_rect().size() });
930 return rect;
931 }
932
933 Gfx::Rect rect;
934 for_each_visual_line(position.line(), [&](const Gfx::Rect& visual_line_rect, const StringView& view, size_t start_of_visual_line) {
935 if (position.column() >= start_of_visual_line && ((position.column() - start_of_visual_line) <= view.length())) {
936 // NOTE: We have to subtract the horizontal padding here since it's part of the visual line rect
937 // *and* included in what we get from content_x_for_position().
938 rect = {
939 visual_line_rect.x() + x - (m_horizontal_content_padding),
940 visual_line_rect.y(),
941 1,
942 line_height()
943 };
944 return IterationDecision::Break;
945 }
946 return IterationDecision::Continue;
947 });
948 return rect;
949}
950
951Gfx::Rect TextEditor::cursor_content_rect() const
952{
953 return content_rect_for_position(m_cursor);
954}
955
956Gfx::Rect TextEditor::line_widget_rect(size_t line_index) const
957{
958 auto rect = line_content_rect(line_index);
959 rect.set_x(frame_thickness());
960 rect.set_width(frame_inner_rect().width());
961 rect.move_by(0, -(vertical_scrollbar().value()));
962 rect.move_by(0, frame_thickness());
963 rect.intersect(frame_inner_rect());
964 return rect;
965}
966
967void TextEditor::scroll_position_into_view(const TextPosition& position)
968{
969 auto rect = content_rect_for_position(position);
970 if (position.column() == 0)
971 rect.set_x(content_x_for_position({ position.line(), 0 }) - 2);
972 else if (position.column() == line(position.line()).length())
973 rect.set_x(content_x_for_position({ position.line(), line(position.line()).length() }) + 2);
974 scroll_into_view(rect, true, true);
975}
976
977void TextEditor::scroll_cursor_into_view()
978{
979 scroll_position_into_view(m_cursor);
980}
981
982Gfx::Rect TextEditor::line_content_rect(size_t line_index) const
983{
984 auto& line = this->line(line_index);
985 if (is_single_line()) {
986 Gfx::Rect line_rect = { content_x_for_position({ line_index, 0 }), 0, (int)line.length() * glyph_width(), font().glyph_height() + 2 };
987 line_rect.center_vertically_within({ {}, frame_inner_rect().size() });
988 return line_rect;
989 }
990 if (is_line_wrapping_enabled())
991 return m_line_visual_data[line_index].visual_rect;
992 return {
993 content_x_for_position({ line_index, 0 }),
994 (int)line_index * line_height(),
995 (int)line.length() * glyph_width(),
996 line_height()
997 };
998}
999
1000void TextEditor::update_cursor()
1001{
1002 update(line_widget_rect(m_cursor.line()));
1003}
1004
1005void TextEditor::set_cursor(size_t line, size_t column)
1006{
1007 set_cursor({ line, column });
1008}
1009
1010void TextEditor::set_cursor(const TextPosition& a_position)
1011{
1012 ASSERT(!lines().is_empty());
1013
1014 TextPosition position = a_position;
1015
1016 if (position.line() >= line_count())
1017 position.set_line(line_count() - 1);
1018
1019 if (position.column() > lines()[position.line()].length())
1020 position.set_column(lines()[position.line()].length());
1021
1022 if (m_cursor != position) {
1023 // NOTE: If the old cursor is no longer valid, repaint everything just in case.
1024 auto old_cursor_line_rect = m_cursor.line() < line_count()
1025 ? line_widget_rect(m_cursor.line())
1026 : rect();
1027 m_cursor = position;
1028 m_cursor_state = true;
1029 scroll_cursor_into_view();
1030 update(old_cursor_line_rect);
1031 update_cursor();
1032 }
1033 cursor_did_change();
1034 if (on_cursor_change)
1035 on_cursor_change();
1036 if (m_highlighter)
1037 m_highlighter->cursor_did_change();
1038}
1039
1040void TextEditor::focusin_event(Core::Event&)
1041{
1042 update_cursor();
1043 start_timer(500);
1044}
1045
1046void TextEditor::focusout_event(Core::Event&)
1047{
1048 stop_timer();
1049}
1050
1051void TextEditor::timer_event(Core::TimerEvent&)
1052{
1053 m_cursor_state = !m_cursor_state;
1054 if (is_focused())
1055 update_cursor();
1056}
1057
1058bool TextEditor::write_to_file(const StringView& path)
1059{
1060 int fd = open_with_path_length(path.characters_without_null_termination(), path.length(), O_WRONLY | O_CREAT | O_TRUNC, 0666);
1061 if (fd < 0) {
1062 perror("open");
1063 return false;
1064 }
1065
1066 // Compute the final file size and ftruncate() to make writing fast.
1067 // FIXME: Remove this once the kernel is smart enough to do this instead.
1068 off_t file_size = 0;
1069 for (size_t i = 0; i < line_count(); ++i)
1070 file_size += line(i).length();
1071 file_size += line_count() - 1;
1072
1073 int rc = ftruncate(fd, file_size);
1074 if (rc < 0) {
1075 perror("ftruncate");
1076 return false;
1077 }
1078
1079 for (size_t i = 0; i < line_count(); ++i) {
1080 auto& line = this->line(i);
1081 if (line.length()) {
1082 ssize_t nwritten = write(fd, line.characters(), line.length());
1083 if (nwritten < 0) {
1084 perror("write");
1085 close(fd);
1086 return false;
1087 }
1088 }
1089 if (i != line_count() - 1) {
1090 char ch = '\n';
1091 ssize_t nwritten = write(fd, &ch, 1);
1092 if (nwritten != 1) {
1093 perror("write");
1094 close(fd);
1095 return false;
1096 }
1097 }
1098 }
1099
1100 close(fd);
1101 return true;
1102}
1103
1104String TextEditor::text() const
1105{
1106 StringBuilder builder;
1107 for (size_t i = 0; i < line_count(); ++i) {
1108 auto& line = this->line(i);
1109 builder.append(line.characters(), line.length());
1110 if (i != line_count() - 1)
1111 builder.append('\n');
1112 }
1113 return builder.to_string();
1114}
1115
1116void TextEditor::clear()
1117{
1118 document().remove_all_lines();
1119 document().append_line(make<TextDocumentLine>(document()));
1120 m_selection.clear();
1121 did_update_selection();
1122 set_cursor(0, 0);
1123 update();
1124}
1125
1126String TextEditor::selected_text() const
1127{
1128 if (!has_selection())
1129 return {};
1130
1131 return document().text_in_range(m_selection);
1132}
1133
1134void TextEditor::delete_selection()
1135{
1136 auto selection = normalized_selection();
1137 execute<RemoveTextCommand>(selected_text(), selection);
1138 m_selection.clear();
1139 did_update_selection();
1140 did_change();
1141 set_cursor(selection.start());
1142 update();
1143}
1144
1145void TextEditor::insert_at_cursor_or_replace_selection(const StringView& text)
1146{
1147 ASSERT(!is_readonly());
1148 if (has_selection())
1149 delete_selection();
1150 execute<InsertTextCommand>(text, m_cursor);
1151}
1152
1153void TextEditor::cut()
1154{
1155 if (is_readonly())
1156 return;
1157 auto selected_text = this->selected_text();
1158 printf("Cut: \"%s\"\n", selected_text.characters());
1159 Clipboard::the().set_data(selected_text);
1160 delete_selection();
1161}
1162
1163void TextEditor::copy()
1164{
1165 auto selected_text = this->selected_text();
1166 printf("Copy: \"%s\"\n", selected_text.characters());
1167 Clipboard::the().set_data(selected_text);
1168}
1169
1170void TextEditor::paste()
1171{
1172 if (is_readonly())
1173 return;
1174 auto paste_text = Clipboard::the().data();
1175 printf("Paste: \"%s\"\n", paste_text.characters());
1176
1177 TemporaryChange change(m_automatic_indentation_enabled, false);
1178 insert_at_cursor_or_replace_selection(paste_text);
1179}
1180
1181void TextEditor::enter_event(Core::Event&)
1182{
1183 ASSERT(window());
1184 window()->set_override_cursor(StandardCursor::IBeam);
1185}
1186
1187void TextEditor::leave_event(Core::Event&)
1188{
1189 ASSERT(window());
1190 window()->set_override_cursor(StandardCursor::None);
1191}
1192
1193void TextEditor::did_change()
1194{
1195 update_content_size();
1196 recompute_all_visual_lines();
1197 m_undo_action->set_enabled(can_undo());
1198 m_redo_action->set_enabled(can_redo());
1199 if (!m_has_pending_change_notification) {
1200 m_has_pending_change_notification = true;
1201 deferred_invoke([this](auto&) {
1202 if (!m_has_pending_change_notification)
1203 return;
1204 if (on_change)
1205 on_change();
1206 if (m_highlighter)
1207 m_highlighter->rehighlight();
1208 m_has_pending_change_notification = false;
1209 });
1210 }
1211}
1212
1213void TextEditor::set_readonly(bool readonly)
1214{
1215 if (m_readonly == readonly)
1216 return;
1217 m_readonly = readonly;
1218 m_cut_action->set_enabled(!is_readonly() && has_selection());
1219 m_delete_action->set_enabled(!is_readonly());
1220 m_paste_action->set_enabled(!is_readonly());
1221}
1222
1223void TextEditor::did_update_selection()
1224{
1225 m_cut_action->set_enabled(!is_readonly() && has_selection());
1226 m_copy_action->set_enabled(has_selection());
1227 if (on_selection_change)
1228 on_selection_change();
1229 if (is_line_wrapping_enabled()) {
1230 // FIXME: Try to repaint less.
1231 update();
1232 }
1233}
1234
1235void TextEditor::context_menu_event(ContextMenuEvent& event)
1236{
1237 if (!m_context_menu) {
1238 m_context_menu = Menu::construct();
1239 m_context_menu->add_action(undo_action());
1240 m_context_menu->add_action(redo_action());
1241 m_context_menu->add_separator();
1242 m_context_menu->add_action(cut_action());
1243 m_context_menu->add_action(copy_action());
1244 m_context_menu->add_action(paste_action());
1245 m_context_menu->add_action(delete_action());
1246 if (is_multi_line()) {
1247 m_context_menu->add_separator();
1248 m_context_menu->add_action(go_to_line_action());
1249 }
1250 if (!m_custom_context_menu_actions.is_empty()) {
1251 m_context_menu->add_separator();
1252 for (auto& action : m_custom_context_menu_actions) {
1253 m_context_menu->add_action(action);
1254 }
1255 }
1256 }
1257 m_context_menu->popup(event.screen_position());
1258}
1259
1260void TextEditor::set_text_alignment(Gfx::TextAlignment alignment)
1261{
1262 if (m_text_alignment == alignment)
1263 return;
1264 m_text_alignment = alignment;
1265 update();
1266}
1267
1268void TextEditor::resize_event(ResizeEvent& event)
1269{
1270 ScrollableWidget::resize_event(event);
1271 update_content_size();
1272 recompute_all_visual_lines();
1273}
1274
1275void TextEditor::set_selection(const TextRange& selection)
1276{
1277 if (m_selection == selection)
1278 return;
1279 m_selection = selection;
1280 set_cursor(m_selection.end());
1281 scroll_position_into_view(normalized_selection().start());
1282 update();
1283}
1284
1285void TextEditor::clear_selection()
1286{
1287 if (!has_selection())
1288 return;
1289 m_selection.clear();
1290 update();
1291}
1292
1293void TextEditor::recompute_all_visual_lines()
1294{
1295 int y_offset = 0;
1296 for (size_t line_index = 0; line_index < line_count(); ++line_index) {
1297 recompute_visual_lines(line_index);
1298 m_line_visual_data[line_index].visual_rect.set_y(y_offset);
1299 y_offset += m_line_visual_data[line_index].visual_rect.height();
1300 }
1301
1302 update_content_size();
1303}
1304
1305void TextEditor::ensure_cursor_is_valid()
1306{
1307 auto new_cursor = m_cursor;
1308 if (new_cursor.line() >= line_count())
1309 new_cursor.set_line(line_count() - 1);
1310 if (new_cursor.column() > line(new_cursor.line()).length())
1311 new_cursor.set_column(line(new_cursor.line()).length());
1312 if (m_cursor != new_cursor)
1313 set_cursor(new_cursor);
1314}
1315
1316size_t TextEditor::visual_line_containing(size_t line_index, size_t column) const
1317{
1318 size_t visual_line_index = 0;
1319 for_each_visual_line(line_index, [&](const Gfx::Rect&, const StringView& view, size_t start_of_visual_line) {
1320 if (column >= start_of_visual_line && ((column - start_of_visual_line) < view.length()))
1321 return IterationDecision::Break;
1322 ++visual_line_index;
1323 return IterationDecision::Continue;
1324 });
1325 return visual_line_index;
1326}
1327
1328void TextEditor::recompute_visual_lines(size_t line_index)
1329{
1330 auto& line = document().line(line_index);
1331 auto& visual_data = m_line_visual_data[line_index];
1332
1333 visual_data.visual_line_breaks.clear_with_capacity();
1334
1335 int available_width = visible_text_rect_in_inner_coordinates().width();
1336
1337 if (is_line_wrapping_enabled()) {
1338 int line_width_so_far = 0;
1339
1340 for (size_t i = 0; i < line.length(); ++i) {
1341 auto ch = line.characters()[i];
1342 auto glyph_width = font().glyph_width(ch);
1343 if ((line_width_so_far + glyph_width) > available_width) {
1344 visual_data.visual_line_breaks.append(i);
1345 line_width_so_far = glyph_width;
1346 continue;
1347 }
1348 line_width_so_far += glyph_width;
1349 }
1350 }
1351
1352 visual_data.visual_line_breaks.append(line.length());
1353
1354 if (is_line_wrapping_enabled())
1355 visual_data.visual_rect = { m_horizontal_content_padding, 0, available_width, static_cast<int>(visual_data.visual_line_breaks.size()) * line_height() };
1356 else
1357 visual_data.visual_rect = { m_horizontal_content_padding, 0, font().width(line.view()), line_height() };
1358}
1359
1360template<typename Callback>
1361void TextEditor::for_each_visual_line(size_t line_index, Callback callback) const
1362{
1363 auto editor_visible_text_rect = visible_text_rect_in_inner_coordinates();
1364 size_t start_of_line = 0;
1365 size_t visual_line_index = 0;
1366
1367 auto& line = document().line(line_index);
1368 auto& visual_data = m_line_visual_data[line_index];
1369
1370 for (auto visual_line_break : visual_data.visual_line_breaks) {
1371 auto visual_line_view = StringView(line.characters() + start_of_line, visual_line_break - start_of_line);
1372 Gfx::Rect visual_line_rect {
1373 visual_data.visual_rect.x(),
1374 visual_data.visual_rect.y() + ((int)visual_line_index * line_height()),
1375 font().width(visual_line_view),
1376 line_height()
1377 };
1378 if (is_right_text_alignment(text_alignment()))
1379 visual_line_rect.set_right_without_resize(editor_visible_text_rect.right());
1380 if (!is_multi_line())
1381 visual_line_rect.center_vertically_within(editor_visible_text_rect);
1382 if (callback(visual_line_rect, visual_line_view, start_of_line) == IterationDecision::Break)
1383 break;
1384 start_of_line = visual_line_break;
1385 ++visual_line_index;
1386 }
1387}
1388
1389void TextEditor::set_line_wrapping_enabled(bool enabled)
1390{
1391 if (m_line_wrapping_enabled == enabled)
1392 return;
1393
1394 m_line_wrapping_enabled = enabled;
1395 horizontal_scrollbar().set_visible(!m_line_wrapping_enabled);
1396 update_content_size();
1397 recompute_all_visual_lines();
1398 update();
1399}
1400
1401void TextEditor::add_custom_context_menu_action(Action& action)
1402{
1403 m_custom_context_menu_actions.append(action);
1404}
1405
1406void TextEditor::did_change_font()
1407{
1408 vertical_scrollbar().set_step(line_height());
1409 recompute_all_visual_lines();
1410 update();
1411 Widget::did_change_font();
1412}
1413
1414void TextEditor::document_did_append_line()
1415{
1416 m_line_visual_data.append(make<LineVisualData>());
1417 recompute_all_visual_lines();
1418 update();
1419}
1420
1421void TextEditor::document_did_remove_line(size_t line_index)
1422{
1423 m_line_visual_data.remove(line_index);
1424 recompute_all_visual_lines();
1425 update();
1426}
1427
1428void TextEditor::document_did_remove_all_lines()
1429{
1430 m_line_visual_data.clear();
1431 recompute_all_visual_lines();
1432 update();
1433}
1434
1435void TextEditor::document_did_insert_line(size_t line_index)
1436{
1437 m_line_visual_data.insert(line_index, make<LineVisualData>());
1438 recompute_all_visual_lines();
1439 update();
1440}
1441
1442void TextEditor::document_did_change()
1443{
1444 did_change();
1445 update();
1446}
1447
1448void TextEditor::document_did_set_text()
1449{
1450 m_line_visual_data.clear();
1451 for (size_t i = 0; i < m_document->line_count(); ++i)
1452 m_line_visual_data.append(make<LineVisualData>());
1453 document_did_change();
1454}
1455
1456void TextEditor::document_did_set_cursor(const TextPosition& position)
1457{
1458 set_cursor(position);
1459}
1460
1461void TextEditor::set_document(TextDocument& document)
1462{
1463 if (m_document.ptr() == &document)
1464 return;
1465 if (m_document)
1466 m_document->unregister_client(*this);
1467 m_document = document;
1468 m_line_visual_data.clear();
1469 for (size_t i = 0; i < m_document->line_count(); ++i) {
1470 m_line_visual_data.append(make<LineVisualData>());
1471 }
1472 m_cursor = { 0, 0 };
1473 if (has_selection())
1474 m_selection.clear();
1475 recompute_all_visual_lines();
1476 update();
1477 m_document->register_client(*this);
1478}
1479
1480void TextEditor::flush_pending_change_notification_if_needed()
1481{
1482 if (!m_has_pending_change_notification)
1483 return;
1484 if (on_change)
1485 on_change();
1486 if (m_highlighter)
1487 m_highlighter->rehighlight();
1488 m_has_pending_change_notification = false;
1489}
1490
1491void TextEditor::set_syntax_highlighter(OwnPtr<SyntaxHighlighter> highlighter)
1492{
1493 if (m_highlighter)
1494 m_highlighter->detach();
1495 m_highlighter = move(highlighter);
1496 if (m_highlighter) {
1497 m_highlighter->attach(*this);
1498 m_highlighter->rehighlight();
1499 }
1500}
1501
1502int TextEditor::line_height() const
1503{
1504 return font().glyph_height() + m_line_spacing;
1505}
1506
1507int TextEditor::glyph_width() const
1508{
1509 return font().glyph_width('x');
1510}
1511
1512}