Serenity Operating System
1/*
2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
3 * Copyright (c) 2022, the SerenityOS developers.
4 * Copyright (c) 2023, Sam Atkins <atkinssj@serenityos.org>
5 *
6 * SPDX-License-Identifier: BSD-2-Clause
7 */
8
9#include <AK/Badge.h>
10#include <AK/CharacterTypes.h>
11#include <AK/Debug.h>
12#include <AK/QuickSort.h>
13#include <AK/ScopeGuard.h>
14#include <AK/StdLibExtras.h>
15#include <AK/StringBuilder.h>
16#include <AK/Utf8View.h>
17#include <LibCore/Timer.h>
18#include <LibGUI/TextDocument.h>
19#include <LibRegex/Regex.h>
20#include <LibUnicode/CharacterTypes.h>
21#include <LibUnicode/Segmentation.h>
22
23namespace GUI {
24
25NonnullRefPtr<TextDocument> TextDocument::create(Client* client)
26{
27 return adopt_ref(*new TextDocument(client));
28}
29
30TextDocument::TextDocument(Client* client)
31{
32 if (client)
33 m_clients.set(client);
34 append_line(make<TextDocumentLine>(*this));
35 set_unmodified();
36
37 m_undo_stack.on_state_change = [this] {
38 if (m_client_notifications_enabled) {
39 for (auto* client : m_clients)
40 client->document_did_update_undo_stack();
41 }
42 };
43}
44
45bool TextDocument::set_text(StringView text, AllowCallback allow_callback)
46{
47 m_client_notifications_enabled = false;
48 m_undo_stack.clear();
49 m_spans.clear();
50 m_folding_regions.clear();
51 remove_all_lines();
52
53 ArmedScopeGuard clear_text_guard([this]() {
54 set_text({});
55 });
56
57 size_t start_of_current_line = 0;
58
59 auto add_line = [&](size_t current_position) -> bool {
60 size_t line_length = current_position - start_of_current_line;
61 auto line = make<TextDocumentLine>(*this);
62
63 bool success = true;
64 if (line_length)
65 success = line->set_text(*this, text.substring_view(start_of_current_line, current_position - start_of_current_line));
66
67 if (!success)
68 return false;
69
70 append_line(move(line));
71 start_of_current_line = current_position + 1;
72
73 return true;
74 };
75
76 size_t i = 0;
77 for (i = 0; i < text.length(); ++i) {
78 if (text[i] != '\n')
79 continue;
80
81 auto success = add_line(i);
82 if (!success)
83 return false;
84 }
85
86 auto success = add_line(i);
87 if (!success)
88 return false;
89
90 // Don't show the file's trailing newline as an actual new line.
91 if (line_count() > 1 && line(line_count() - 1).is_empty())
92 (void)m_lines.take_last();
93
94 m_client_notifications_enabled = true;
95
96 for (auto* client : m_clients)
97 client->document_did_set_text(allow_callback);
98
99 clear_text_guard.disarm();
100
101 // FIXME: Should the modified state be cleared on some of the earlier returns as well?
102 set_unmodified();
103 return true;
104}
105
106size_t TextDocumentLine::first_non_whitespace_column() const
107{
108 for (size_t i = 0; i < length(); ++i) {
109 auto code_point = code_points()[i];
110 if (!is_ascii_space(code_point))
111 return i;
112 }
113 return length();
114}
115
116Optional<size_t> TextDocumentLine::last_non_whitespace_column() const
117{
118 for (ssize_t i = length() - 1; i >= 0; --i) {
119 auto code_point = code_points()[i];
120 if (!is_ascii_space(code_point))
121 return i;
122 }
123 return {};
124}
125
126bool TextDocumentLine::ends_in_whitespace() const
127{
128 if (!length())
129 return false;
130 return is_ascii_space(code_points()[length() - 1]);
131}
132
133bool TextDocumentLine::can_select() const
134{
135 if (is_empty())
136 return false;
137 for (size_t i = 0; i < length(); ++i) {
138 auto code_point = code_points()[i];
139 if (code_point != '\n' && code_point != '\r' && code_point != '\f' && code_point != '\v')
140 return true;
141 }
142 return false;
143}
144
145size_t TextDocumentLine::leading_spaces() const
146{
147 size_t count = 0;
148 for (; count < m_text.size(); ++count) {
149 if (m_text[count] != ' ') {
150 break;
151 }
152 }
153 return count;
154}
155
156DeprecatedString TextDocumentLine::to_utf8() const
157{
158 StringBuilder builder;
159 builder.append(view());
160 return builder.to_deprecated_string();
161}
162
163TextDocumentLine::TextDocumentLine(TextDocument& document)
164{
165 clear(document);
166}
167
168TextDocumentLine::TextDocumentLine(TextDocument& document, StringView text)
169{
170 set_text(document, text);
171}
172
173void TextDocumentLine::clear(TextDocument& document)
174{
175 m_text.clear();
176 document.update_views({});
177}
178
179void TextDocumentLine::set_text(TextDocument& document, Vector<u32> const text)
180{
181 m_text = move(text);
182 document.update_views({});
183}
184
185bool TextDocumentLine::set_text(TextDocument& document, StringView text)
186{
187 if (text.is_empty()) {
188 clear(document);
189 return true;
190 }
191 m_text.clear();
192 Utf8View utf8_view(text);
193 if (!utf8_view.validate()) {
194 return false;
195 }
196 for (auto code_point : utf8_view)
197 m_text.append(code_point);
198 document.update_views({});
199 return true;
200}
201
202void TextDocumentLine::append(TextDocument& document, u32 const* code_points, size_t length)
203{
204 if (length == 0)
205 return;
206 m_text.append(code_points, length);
207 document.update_views({});
208}
209
210void TextDocumentLine::append(TextDocument& document, u32 code_point)
211{
212 insert(document, length(), code_point);
213}
214
215void TextDocumentLine::prepend(TextDocument& document, u32 code_point)
216{
217 insert(document, 0, code_point);
218}
219
220void TextDocumentLine::insert(TextDocument& document, size_t index, u32 code_point)
221{
222 if (index == length()) {
223 m_text.append(code_point);
224 } else {
225 m_text.insert(index, code_point);
226 }
227 document.update_views({});
228}
229
230void TextDocumentLine::remove(TextDocument& document, size_t index)
231{
232 if (index == length()) {
233 m_text.take_last();
234 } else {
235 m_text.remove(index);
236 }
237 document.update_views({});
238}
239
240void TextDocumentLine::remove_range(TextDocument& document, size_t start, size_t length)
241{
242 VERIFY(length <= m_text.size());
243
244 Vector<u32> new_data;
245 new_data.ensure_capacity(m_text.size() - length);
246 for (size_t i = 0; i < start; ++i)
247 new_data.append(m_text[i]);
248 for (size_t i = (start + length); i < m_text.size(); ++i)
249 new_data.append(m_text[i]);
250 m_text = move(new_data);
251 document.update_views({});
252}
253
254void TextDocumentLine::keep_range(TextDocument& document, size_t start_index, size_t length)
255{
256 VERIFY(start_index + length < m_text.size());
257
258 Vector<u32> new_data;
259 new_data.ensure_capacity(m_text.size());
260 for (size_t i = start_index; i <= (start_index + length); i++)
261 new_data.append(m_text[i]);
262
263 m_text = move(new_data);
264 document.update_views({});
265}
266
267void TextDocumentLine::truncate(TextDocument& document, size_t length)
268{
269 m_text.resize(length);
270 document.update_views({});
271}
272
273void TextDocument::append_line(NonnullOwnPtr<TextDocumentLine> line)
274{
275 lines().append(move(line));
276 if (m_client_notifications_enabled) {
277 for (auto* client : m_clients)
278 client->document_did_append_line();
279 }
280}
281
282void TextDocument::insert_line(size_t line_index, NonnullOwnPtr<TextDocumentLine> line)
283{
284 lines().insert(line_index, move(line));
285 if (m_client_notifications_enabled) {
286 for (auto* client : m_clients)
287 client->document_did_insert_line(line_index);
288 }
289}
290
291NonnullOwnPtr<TextDocumentLine> TextDocument::take_line(size_t line_index)
292{
293 auto line = lines().take(line_index);
294 if (m_client_notifications_enabled) {
295 for (auto* client : m_clients)
296 client->document_did_remove_line(line_index);
297 }
298 return line;
299}
300
301void TextDocument::remove_line(size_t line_index)
302{
303 lines().remove(line_index);
304 if (m_client_notifications_enabled) {
305 for (auto* client : m_clients)
306 client->document_did_remove_line(line_index);
307 }
308}
309
310void TextDocument::remove_all_lines()
311{
312 lines().clear();
313 if (m_client_notifications_enabled) {
314 for (auto* client : m_clients)
315 client->document_did_remove_all_lines();
316 }
317}
318
319void TextDocument::register_client(Client& client)
320{
321 m_clients.set(&client);
322}
323
324void TextDocument::unregister_client(Client& client)
325{
326 m_clients.remove(&client);
327}
328
329void TextDocument::update_views(Badge<TextDocumentLine>)
330{
331 notify_did_change();
332}
333
334void TextDocument::notify_did_change()
335{
336 if (m_client_notifications_enabled) {
337 for (auto* client : m_clients)
338 client->document_did_change();
339 }
340
341 m_regex_needs_update = true;
342}
343
344void TextDocument::set_all_cursors(TextPosition const& position)
345{
346 if (m_client_notifications_enabled) {
347 for (auto* client : m_clients)
348 client->document_did_set_cursor(position);
349 }
350}
351
352DeprecatedString TextDocument::text() const
353{
354 StringBuilder builder;
355 for (size_t i = 0; i < line_count(); ++i) {
356 auto& line = this->line(i);
357 builder.append(line.view());
358 if (i != line_count() - 1)
359 builder.append('\n');
360 }
361 return builder.to_deprecated_string();
362}
363
364DeprecatedString TextDocument::text_in_range(TextRange const& a_range) const
365{
366 auto range = a_range.normalized();
367 if (is_empty() || line_count() < range.end().line() - range.start().line())
368 return DeprecatedString("");
369
370 StringBuilder builder;
371 for (size_t i = range.start().line(); i <= range.end().line(); ++i) {
372 auto& line = this->line(i);
373 size_t selection_start_column_on_line = range.start().line() == i ? range.start().column() : 0;
374 size_t selection_end_column_on_line = range.end().line() == i ? range.end().column() : line.length();
375
376 if (!line.is_empty()) {
377 builder.append(
378 Utf32View(
379 line.code_points() + selection_start_column_on_line,
380 selection_end_column_on_line - selection_start_column_on_line));
381 }
382
383 if (i != range.end().line())
384 builder.append('\n');
385 }
386
387 return builder.to_deprecated_string();
388}
389
390// This function will return the position of the previous grapheme cluster
391// break, relative to the cursor, for "correct looking" parsing of unicode based
392// on grapheme cluster boundary algorithm.
393size_t TextDocument::get_previous_grapheme_cluster_boundary(TextPosition const& cursor) const
394{
395 if (!cursor.is_valid())
396 return 0;
397
398 auto const& line = this->line(cursor.line());
399
400 auto index = Unicode::previous_grapheme_segmentation_boundary(line.view(), cursor.column());
401 return index.value_or(cursor.column() - 1);
402}
403
404// This function will return the position of the next grapheme cluster break,
405// relative to the cursor, for "correct looking" parsing of unicode based on
406// grapheme cluster boundary algorithm.
407size_t TextDocument::get_next_grapheme_cluster_boundary(TextPosition const& cursor) const
408{
409 if (!cursor.is_valid())
410 return 0;
411
412 auto const& line = this->line(cursor.line());
413
414 auto index = Unicode::next_grapheme_segmentation_boundary(line.view(), cursor.column());
415 return index.value_or(cursor.column() + 1);
416}
417
418u32 TextDocument::code_point_at(TextPosition const& position) const
419{
420 VERIFY(position.line() < line_count());
421 auto& line = this->line(position.line());
422 if (position.column() == line.length())
423 return '\n';
424 return line.code_points()[position.column()];
425}
426
427TextPosition TextDocument::next_position_after(TextPosition const& position, SearchShouldWrap should_wrap) const
428{
429 auto& line = this->line(position.line());
430 if (position.column() == line.length()) {
431 if (position.line() == line_count() - 1) {
432 if (should_wrap == SearchShouldWrap::Yes)
433 return { 0, 0 };
434 return {};
435 }
436 return { position.line() + 1, 0 };
437 }
438 return { position.line(), position.column() + 1 };
439}
440
441TextPosition TextDocument::previous_position_before(TextPosition const& position, SearchShouldWrap should_wrap) const
442{
443 if (position.column() == 0) {
444 if (position.line() == 0) {
445 if (should_wrap == SearchShouldWrap::Yes) {
446 auto& last_line = this->line(line_count() - 1);
447 return { line_count() - 1, last_line.length() };
448 }
449 return {};
450 }
451 auto& prev_line = this->line(position.line() - 1);
452 return { position.line() - 1, prev_line.length() };
453 }
454 return { position.line(), position.column() - 1 };
455}
456
457void TextDocument::update_regex_matches(StringView needle)
458{
459 if (m_regex_needs_update || needle != m_regex_needle) {
460 Regex<PosixExtended> re(needle);
461
462 Vector<RegexStringView> views;
463
464 for (size_t line = 0; line < m_lines.size(); ++line) {
465 views.append(m_lines[line]->view());
466 }
467 re.search(views, m_regex_result);
468 m_regex_needs_update = false;
469 m_regex_needle = DeprecatedString { needle };
470 m_regex_result_match_index = -1;
471 m_regex_result_match_capture_group_index = -1;
472 }
473}
474
475TextRange TextDocument::find_next(StringView needle, TextPosition const& start, SearchShouldWrap should_wrap, bool regmatch, bool match_case)
476{
477 if (needle.is_empty())
478 return {};
479
480 if (regmatch) {
481 if (!m_regex_result.matches.size())
482 return {};
483
484 regex::Match match;
485 bool use_whole_match { false };
486
487 auto next_match = [&] {
488 m_regex_result_match_capture_group_index = 0;
489 if (m_regex_result_match_index == m_regex_result.matches.size() - 1) {
490 if (should_wrap == SearchShouldWrap::Yes)
491 m_regex_result_match_index = 0;
492 else
493 ++m_regex_result_match_index;
494 } else
495 ++m_regex_result_match_index;
496 };
497
498 if (m_regex_result.n_capture_groups) {
499 if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
500 next_match();
501 else {
502 // check if last capture group has been reached
503 if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size()) {
504 next_match();
505 } else {
506 // get to the next capture group item
507 ++m_regex_result_match_capture_group_index;
508 }
509 }
510
511 // use whole match, if there is no capture group for current index
512 if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
513 use_whole_match = true;
514 else if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
515 next_match();
516
517 } else {
518 next_match();
519 }
520
521 if (use_whole_match || !m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
522 match = m_regex_result.matches.at(m_regex_result_match_index);
523 else
524 match = m_regex_result.capture_group_matches.at(m_regex_result_match_index).at(m_regex_result_match_capture_group_index);
525
526 return TextRange { { match.line, match.column }, { match.line, match.column + match.view.length() } };
527 }
528
529 TextPosition position = start.is_valid() ? start : TextPosition(0, 0);
530 TextPosition original_position = position;
531
532 TextPosition start_of_potential_match;
533 size_t needle_index = 0;
534
535 Utf8View unicode_needle(needle);
536 Vector<u32> needle_code_points;
537 for (u32 code_point : unicode_needle)
538 needle_code_points.append(code_point);
539
540 do {
541 auto ch = code_point_at(position);
542
543 bool code_point_matches = false;
544 if (needle_index >= needle_code_points.size())
545 code_point_matches = false;
546 else if (match_case)
547 code_point_matches = ch == needle_code_points[needle_index];
548 else
549 code_point_matches = Unicode::to_unicode_lowercase(ch) == Unicode::to_unicode_lowercase(needle_code_points[needle_index]);
550
551 if (code_point_matches) {
552 if (needle_index == 0)
553 start_of_potential_match = position;
554 ++needle_index;
555 if (needle_index >= needle_code_points.size())
556 return { start_of_potential_match, next_position_after(position, should_wrap) };
557 } else {
558 if (needle_index > 0)
559 position = start_of_potential_match;
560 needle_index = 0;
561 }
562 position = next_position_after(position, should_wrap);
563 } while (position.is_valid() && position != original_position);
564
565 return {};
566}
567
568TextRange TextDocument::find_previous(StringView needle, TextPosition const& start, SearchShouldWrap should_wrap, bool regmatch, bool match_case)
569{
570 if (needle.is_empty())
571 return {};
572
573 if (regmatch) {
574 if (!m_regex_result.matches.size())
575 return {};
576
577 regex::Match match;
578 bool use_whole_match { false };
579
580 auto next_match = [&] {
581 if (m_regex_result_match_index == 0) {
582 if (should_wrap == SearchShouldWrap::Yes)
583 m_regex_result_match_index = m_regex_result.matches.size() - 1;
584 else
585 --m_regex_result_match_index;
586 } else
587 --m_regex_result_match_index;
588
589 m_regex_result_match_capture_group_index = m_regex_result.capture_group_matches.at(m_regex_result_match_index).size() - 1;
590 };
591
592 if (m_regex_result.n_capture_groups) {
593 if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
594 next_match();
595 else {
596 // check if last capture group has been reached
597 if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size()) {
598 next_match();
599 } else {
600 // get to the next capture group item
601 --m_regex_result_match_capture_group_index;
602 }
603 }
604
605 // use whole match, if there is no capture group for current index
606 if (m_regex_result_match_index >= m_regex_result.capture_group_matches.size())
607 use_whole_match = true;
608 else if (m_regex_result_match_capture_group_index >= m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
609 next_match();
610
611 } else {
612 next_match();
613 }
614
615 if (use_whole_match || !m_regex_result.capture_group_matches.at(m_regex_result_match_index).size())
616 match = m_regex_result.matches.at(m_regex_result_match_index);
617 else
618 match = m_regex_result.capture_group_matches.at(m_regex_result_match_index).at(m_regex_result_match_capture_group_index);
619
620 return TextRange { { match.line, match.column }, { match.line, match.column + match.view.length() } };
621 }
622
623 TextPosition position = start.is_valid() ? start : TextPosition(0, 0);
624 position = previous_position_before(position, should_wrap);
625 if (position.line() >= line_count())
626 return {};
627 TextPosition original_position = position;
628
629 Utf8View unicode_needle(needle);
630 Vector<u32> needle_code_points;
631 for (u32 code_point : unicode_needle)
632 needle_code_points.append(code_point);
633
634 TextPosition end_of_potential_match;
635 size_t needle_index = needle_code_points.size() - 1;
636
637 do {
638 auto ch = code_point_at(position);
639
640 bool code_point_matches = false;
641 if (needle_index >= needle_code_points.size())
642 code_point_matches = false;
643 else if (match_case)
644 code_point_matches = ch == needle_code_points[needle_index];
645 else
646 code_point_matches = Unicode::to_unicode_lowercase(ch) == Unicode::to_unicode_lowercase(needle_code_points[needle_index]);
647
648 if (code_point_matches) {
649 if (needle_index == needle_code_points.size() - 1)
650 end_of_potential_match = position;
651 if (needle_index == 0)
652 return { position, next_position_after(end_of_potential_match, should_wrap) };
653 --needle_index;
654 } else {
655 if (needle_index < needle_code_points.size() - 1)
656 position = end_of_potential_match;
657 needle_index = needle_code_points.size() - 1;
658 }
659 position = previous_position_before(position, should_wrap);
660 } while (position.is_valid() && position != original_position);
661
662 return {};
663}
664
665Vector<TextRange> TextDocument::find_all(StringView needle, bool regmatch, bool match_case)
666{
667 Vector<TextRange> ranges;
668
669 TextPosition position;
670 for (;;) {
671 auto range = find_next(needle, position, SearchShouldWrap::No, regmatch, match_case);
672 if (!range.is_valid())
673 break;
674 ranges.append(range);
675 position = range.end();
676 }
677 return ranges;
678}
679
680Optional<TextDocumentSpan> TextDocument::first_non_skippable_span_before(TextPosition const& position) const
681{
682 for (int i = m_spans.size() - 1; i >= 0; --i) {
683 if (!m_spans[i].range.contains(position))
684 continue;
685 while ((i - 1) >= 0 && m_spans[i - 1].is_skippable)
686 --i;
687 if (i <= 0)
688 return {};
689 return m_spans[i - 1];
690 }
691 return {};
692}
693
694Optional<TextDocumentSpan> TextDocument::first_non_skippable_span_after(TextPosition const& position) const
695{
696 size_t i = 0;
697 // Find the first span containing the cursor
698 for (; i < m_spans.size(); ++i) {
699 if (m_spans[i].range.contains(position))
700 break;
701 }
702 // Find the first span *after* the cursor
703 // TODO: For a large number of spans, binary search would be faster.
704 for (; i < m_spans.size(); ++i) {
705 if (!m_spans[i].range.contains(position))
706 break;
707 }
708 // Skip skippable spans
709 for (; i < m_spans.size(); ++i) {
710 if (!m_spans[i].is_skippable)
711 break;
712 }
713 if (i < m_spans.size())
714 return m_spans[i];
715 return {};
716}
717
718static bool should_continue_beyond_word(Utf32View const& view)
719{
720 static auto punctuation = Unicode::general_category_from_string("Punctuation"sv);
721 static auto separator = Unicode::general_category_from_string("Separator"sv);
722
723 if (!punctuation.has_value() || !separator.has_value())
724 return false;
725
726 auto has_any_gc = [&](auto code_point, auto&&... categories) {
727 return (Unicode::code_point_has_general_category(code_point, *categories) || ...);
728 };
729
730 for (auto code_point : view) {
731 if (!has_any_gc(code_point, punctuation, separator))
732 return false;
733 }
734
735 return true;
736}
737
738TextPosition TextDocument::first_word_break_before(TextPosition const& position, bool start_at_column_before) const
739{
740 if (position.column() == 0) {
741 if (position.line() == 0) {
742 return TextPosition(0, 0);
743 }
744 auto previous_line = this->line(position.line() - 1);
745 return TextPosition(position.line() - 1, previous_line.length());
746 }
747
748 auto target = position;
749 auto const& line = this->line(target.line());
750
751 auto modifier = start_at_column_before ? 1 : 0;
752 if (target.column() == line.length())
753 modifier = 1;
754
755 target.set_column(target.column() - modifier);
756
757 while (target.column() < line.length()) {
758 if (auto index = Unicode::previous_word_segmentation_boundary(line.view(), target.column()); index.has_value()) {
759 auto view_between_target_and_index = line.view().substring_view(*index, target.column() - *index);
760
761 if (should_continue_beyond_word(view_between_target_and_index)) {
762 target.set_column(*index - 1);
763 continue;
764 }
765
766 target.set_column(*index);
767 break;
768 }
769 }
770
771 return target;
772}
773
774TextPosition TextDocument::first_word_break_after(TextPosition const& position) const
775{
776 auto target = position;
777 auto const& line = this->line(target.line());
778
779 if (position.column() >= line.length()) {
780 if (position.line() >= this->line_count() - 1) {
781 return position;
782 }
783 return TextPosition(position.line() + 1, 0);
784 }
785
786 while (target.column() < line.length()) {
787 if (auto index = Unicode::next_word_segmentation_boundary(line.view(), target.column()); index.has_value()) {
788 auto view_between_target_and_index = line.view().substring_view(target.column(), *index - target.column());
789
790 if (should_continue_beyond_word(view_between_target_and_index)) {
791 target.set_column(*index + 1);
792 continue;
793 }
794
795 target.set_column(*index);
796 break;
797 }
798 }
799
800 return target;
801}
802
803TextPosition TextDocument::first_word_before(TextPosition const& position, bool start_at_column_before) const
804{
805 if (position.column() == 0) {
806 if (position.line() == 0) {
807 return TextPosition(0, 0);
808 }
809 auto previous_line = this->line(position.line() - 1);
810 return TextPosition(position.line() - 1, previous_line.length());
811 }
812
813 auto target = position;
814 auto line = this->line(target.line());
815 if (target.column() == line.length())
816 start_at_column_before = 1;
817
818 auto nonblank_passed = !is_ascii_blank(line.code_points()[target.column() - start_at_column_before]);
819 while (target.column() > 0) {
820 auto prev_code_point = line.code_points()[target.column() - 1];
821 nonblank_passed |= !is_ascii_blank(prev_code_point);
822
823 if (nonblank_passed && is_ascii_blank(prev_code_point)) {
824 break;
825 } else if (is_ascii_punctuation(prev_code_point)) {
826 target.set_column(target.column() - 1);
827 break;
828 }
829
830 target.set_column(target.column() - 1);
831 }
832
833 return target;
834}
835
836void TextDocument::undo()
837{
838 if (!can_undo())
839 return;
840 m_undo_stack.undo();
841 notify_did_change();
842}
843
844void TextDocument::redo()
845{
846 if (!can_redo())
847 return;
848 m_undo_stack.redo();
849 notify_did_change();
850}
851
852void TextDocument::add_to_undo_stack(NonnullOwnPtr<TextDocumentUndoCommand> undo_command)
853{
854 m_undo_stack.push(move(undo_command));
855}
856
857TextDocumentUndoCommand::TextDocumentUndoCommand(TextDocument& document)
858 : m_document(document)
859{
860}
861
862InsertTextCommand::InsertTextCommand(TextDocument& document, DeprecatedString const& text, TextPosition const& position)
863 : TextDocumentUndoCommand(document)
864 , m_text(text)
865 , m_range({ position, position })
866{
867}
868
869DeprecatedString InsertTextCommand::action_text() const
870{
871 return "Insert Text";
872}
873
874bool InsertTextCommand::merge_with(GUI::Command const& other)
875{
876 if (!is<InsertTextCommand>(other) || commit_time_expired())
877 return false;
878
879 auto const& typed_other = static_cast<InsertTextCommand const&>(other);
880 if (typed_other.m_text.is_whitespace() && !m_text.is_whitespace())
881 return false; // Skip if other is whitespace while this is not
882
883 if (m_range.end() != typed_other.m_range.start())
884 return false;
885 if (m_range.start().line() != m_range.end().line())
886 return false;
887
888 StringBuilder builder(m_text.length() + typed_other.m_text.length());
889 builder.append(m_text);
890 builder.append(typed_other.m_text);
891 m_text = builder.to_deprecated_string();
892 m_range.set_end(typed_other.m_range.end());
893
894 m_timestamp = Time::now_monotonic();
895 return true;
896}
897
898void InsertTextCommand::perform_formatting(TextDocument::Client const& client)
899{
900 const size_t tab_width = client.soft_tab_width();
901 auto const& dest_line = m_document.line(m_range.start().line());
902 bool const should_auto_indent = client.is_automatic_indentation_enabled();
903
904 StringBuilder builder;
905 size_t column = m_range.start().column();
906 size_t line_indentation = dest_line.leading_spaces();
907 bool at_start_of_line = line_indentation == column;
908
909 for (auto input_char : m_text) {
910 if (input_char == '\n') {
911 size_t spaces_at_end = 0;
912 if (column < line_indentation)
913 spaces_at_end = line_indentation - column;
914 line_indentation -= spaces_at_end;
915 builder.append('\n');
916 column = 0;
917 if (should_auto_indent) {
918 for (; column < line_indentation; ++column) {
919 builder.append(' ');
920 }
921 }
922 at_start_of_line = true;
923 } else if (input_char == '\t') {
924 size_t next_soft_tab_stop = ((column + tab_width) / tab_width) * tab_width;
925 size_t spaces_to_insert = next_soft_tab_stop - column;
926 for (size_t i = 0; i < spaces_to_insert; ++i) {
927 builder.append(' ');
928 }
929 column = next_soft_tab_stop;
930 if (at_start_of_line) {
931 line_indentation = column;
932 }
933 } else {
934 if (input_char == ' ') {
935 if (at_start_of_line) {
936 ++line_indentation;
937 }
938 } else {
939 at_start_of_line = false;
940 }
941 builder.append(input_char);
942 ++column;
943 }
944 }
945 m_text = builder.to_deprecated_string();
946}
947
948void InsertTextCommand::redo()
949{
950 auto new_cursor = m_document.insert_at(m_range.start(), m_text, m_client);
951 // NOTE: We don't know where the range ends until after doing redo().
952 // This is okay since we always do redo() after adding this to the undo stack.
953 m_range.set_end(new_cursor);
954 m_document.set_all_cursors(new_cursor);
955}
956
957void InsertTextCommand::undo()
958{
959 m_document.remove(m_range);
960 m_document.set_all_cursors(m_range.start());
961}
962
963RemoveTextCommand::RemoveTextCommand(TextDocument& document, DeprecatedString const& text, TextRange const& range)
964 : TextDocumentUndoCommand(document)
965 , m_text(text)
966 , m_range(range)
967{
968}
969
970DeprecatedString RemoveTextCommand::action_text() const
971{
972 return "Remove Text";
973}
974
975bool RemoveTextCommand::merge_with(GUI::Command const& other)
976{
977 if (!is<RemoveTextCommand>(other) || commit_time_expired())
978 return false;
979
980 auto const& typed_other = static_cast<RemoveTextCommand const&>(other);
981
982 if (m_range.start() != typed_other.m_range.end())
983 return false;
984 if (m_range.start().line() != m_range.end().line())
985 return false;
986
987 // Merge backspaces
988 StringBuilder builder(m_text.length() + typed_other.m_text.length());
989 builder.append(typed_other.m_text);
990 builder.append(m_text);
991 m_text = builder.to_deprecated_string();
992 m_range.set_start(typed_other.m_range.start());
993
994 m_timestamp = Time::now_monotonic();
995 return true;
996}
997
998void RemoveTextCommand::redo()
999{
1000 m_document.remove(m_range);
1001 m_document.set_all_cursors(m_range.start());
1002}
1003
1004void RemoveTextCommand::undo()
1005{
1006 auto new_cursor = m_document.insert_at(m_range.start(), m_text);
1007 m_document.set_all_cursors(new_cursor);
1008}
1009
1010InsertLineCommand::InsertLineCommand(TextDocument& document, TextPosition cursor, DeprecatedString&& text, InsertPosition pos)
1011 : TextDocumentUndoCommand(document)
1012 , m_cursor(cursor)
1013 , m_text(move(text))
1014 , m_pos(pos)
1015{
1016}
1017
1018void InsertLineCommand::redo()
1019{
1020 size_t line_number = compute_line_number();
1021 m_document.insert_line(line_number, make<TextDocumentLine>(m_document, m_text));
1022 m_document.set_all_cursors(TextPosition { line_number, m_document.line(line_number).length() });
1023}
1024
1025void InsertLineCommand::undo()
1026{
1027 size_t line_number = compute_line_number();
1028 m_document.remove_line(line_number);
1029 m_document.set_all_cursors(m_cursor);
1030}
1031
1032size_t InsertLineCommand::compute_line_number() const
1033{
1034 if (m_pos == InsertPosition::Above)
1035 return m_cursor.line();
1036
1037 if (m_pos == InsertPosition::Below)
1038 return m_cursor.line() + 1;
1039
1040 VERIFY_NOT_REACHED();
1041}
1042
1043DeprecatedString InsertLineCommand::action_text() const
1044{
1045 StringBuilder action_text_builder;
1046 action_text_builder.append("Insert Line"sv);
1047
1048 if (m_pos == InsertPosition::Above) {
1049 action_text_builder.append(" (Above)"sv);
1050 } else if (m_pos == InsertPosition::Below) {
1051 action_text_builder.append(" (Below)"sv);
1052 } else {
1053 VERIFY_NOT_REACHED();
1054 }
1055
1056 return action_text_builder.to_deprecated_string();
1057}
1058
1059ReplaceAllTextCommand::ReplaceAllTextCommand(GUI::TextDocument& document, DeprecatedString const& text, GUI::TextRange const& range, DeprecatedString const& action_text)
1060 : TextDocumentUndoCommand(document)
1061 , m_original_text(document.text())
1062 , m_new_text(text)
1063 , m_range(range)
1064 , m_action_text(action_text)
1065{
1066}
1067
1068void ReplaceAllTextCommand::redo()
1069{
1070 m_document.remove(m_range);
1071 m_document.set_all_cursors(m_range.start());
1072 auto new_cursor = m_document.insert_at(m_range.start(), m_new_text, m_client);
1073 m_range.set_end(new_cursor);
1074 m_document.set_all_cursors(new_cursor);
1075}
1076
1077void ReplaceAllTextCommand::undo()
1078{
1079 m_document.remove(m_range);
1080 m_document.set_all_cursors(m_range.start());
1081 auto new_cursor = m_document.insert_at(m_range.start(), m_original_text, m_client);
1082 m_range.set_end(new_cursor);
1083 m_document.set_all_cursors(new_cursor);
1084}
1085
1086bool ReplaceAllTextCommand::merge_with(GUI::Command const&)
1087{
1088 return false;
1089}
1090
1091DeprecatedString ReplaceAllTextCommand::action_text() const
1092{
1093 return m_action_text;
1094}
1095
1096IndentSelection::IndentSelection(TextDocument& document, size_t tab_width, TextRange const& range)
1097 : TextDocumentUndoCommand(document)
1098 , m_tab_width(tab_width)
1099 , m_range(range)
1100{
1101}
1102
1103void IndentSelection::redo()
1104{
1105 auto const tab = DeprecatedString::repeated(' ', m_tab_width);
1106
1107 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1108 m_document.insert_at({ i, 0 }, tab, m_client);
1109 }
1110
1111 m_document.set_all_cursors(m_range.start());
1112}
1113
1114void IndentSelection::undo()
1115{
1116 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1117 m_document.remove({ { i, 0 }, { i, m_tab_width } });
1118 }
1119
1120 m_document.set_all_cursors(m_range.start());
1121}
1122
1123UnindentSelection::UnindentSelection(TextDocument& document, size_t tab_width, TextRange const& range)
1124 : TextDocumentUndoCommand(document)
1125 , m_tab_width(tab_width)
1126 , m_range(range)
1127{
1128}
1129
1130void UnindentSelection::redo()
1131{
1132 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1133 if (m_document.line(i).leading_spaces() >= m_tab_width)
1134 m_document.remove({ { i, 0 }, { i, m_tab_width } });
1135 else
1136 m_document.remove({ { i, 0 }, { i, m_document.line(i).leading_spaces() } });
1137 }
1138
1139 m_document.set_all_cursors(m_range.start());
1140}
1141
1142void UnindentSelection::undo()
1143{
1144 auto const tab = DeprecatedString::repeated(' ', m_tab_width);
1145
1146 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++)
1147 m_document.insert_at({ i, 0 }, tab, m_client);
1148
1149 m_document.set_all_cursors(m_range.start());
1150}
1151
1152CommentSelection::CommentSelection(TextDocument& document, StringView prefix, StringView suffix, TextRange const& range)
1153 : TextDocumentUndoCommand(document)
1154 , m_prefix(prefix)
1155 , m_suffix(suffix)
1156 , m_range(range)
1157{
1158}
1159
1160void CommentSelection::undo()
1161{
1162 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1163 if (m_document.line(i).is_empty())
1164 continue;
1165 auto line = m_document.line(i).to_utf8();
1166 auto prefix_start = line.find(m_prefix).value_or(0);
1167 m_document.line(i).keep_range(
1168 m_document,
1169 prefix_start + m_prefix.length(),
1170 m_document.line(i).last_non_whitespace_column().value_or(line.length()) - prefix_start - m_prefix.length() - m_suffix.length());
1171 }
1172 m_document.set_all_cursors(m_range.start());
1173}
1174
1175void CommentSelection::redo()
1176{
1177 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1178 if (m_document.line(i).is_empty())
1179 continue;
1180 m_document.insert_at({ i, 0 }, m_prefix, m_client);
1181 for (auto const& b : m_suffix.bytes()) {
1182 m_document.line(i).append(m_document, b);
1183 }
1184 }
1185 m_document.set_all_cursors(m_range.start());
1186}
1187
1188UncommentSelection::UncommentSelection(TextDocument& document, StringView prefix, StringView suffix, TextRange const& range)
1189 : TextDocumentUndoCommand(document)
1190 , m_prefix(prefix)
1191 , m_suffix(suffix)
1192 , m_range(range)
1193{
1194}
1195
1196void UncommentSelection::undo()
1197{
1198 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1199 if (m_document.line(i).is_empty())
1200 continue;
1201 m_document.insert_at({ i, 0 }, m_prefix, m_client);
1202 for (auto const& b : m_suffix.bytes()) {
1203 m_document.line(i).append(m_document, b);
1204 }
1205 }
1206 m_document.set_all_cursors(m_range.start());
1207}
1208
1209void UncommentSelection::redo()
1210{
1211 for (size_t i = m_range.start().line(); i <= m_range.end().line(); i++) {
1212 if (m_document.line(i).is_empty())
1213 continue;
1214 auto line = m_document.line(i).to_utf8();
1215 auto prefix_start = line.find(m_prefix).value_or(0);
1216 m_document.line(i).keep_range(
1217 m_document,
1218 prefix_start + m_prefix.length(),
1219 m_document.line(i).last_non_whitespace_column().value_or(line.length()) - prefix_start - m_prefix.length() - m_suffix.length());
1220 }
1221 m_document.set_all_cursors(m_range.start());
1222}
1223
1224TextPosition TextDocument::insert_at(TextPosition const& position, StringView text, Client const* client)
1225{
1226 TextPosition cursor = position;
1227 Utf8View utf8_view(text);
1228 for (auto code_point : utf8_view)
1229 cursor = insert_at(cursor, code_point, client);
1230 return cursor;
1231}
1232
1233TextPosition TextDocument::insert_at(TextPosition const& position, u32 code_point, Client const*)
1234{
1235 if (code_point == '\n') {
1236 auto new_line = make<TextDocumentLine>(*this);
1237 new_line->append(*this, line(position.line()).code_points() + position.column(), line(position.line()).length() - position.column());
1238 line(position.line()).truncate(*this, position.column());
1239 insert_line(position.line() + 1, move(new_line));
1240 notify_did_change();
1241 return { position.line() + 1, 0 };
1242 } else {
1243 line(position.line()).insert(*this, position.column(), code_point);
1244 notify_did_change();
1245 return { position.line(), position.column() + 1 };
1246 }
1247}
1248
1249void TextDocument::remove(TextRange const& unnormalized_range)
1250{
1251 if (!unnormalized_range.is_valid())
1252 return;
1253
1254 auto range = unnormalized_range.normalized();
1255
1256 // First delete all the lines in between the first and last one.
1257 for (size_t i = range.start().line() + 1; i < range.end().line();) {
1258 remove_line(i);
1259 range.end().set_line(range.end().line() - 1);
1260 }
1261
1262 if (range.start().line() == range.end().line()) {
1263 // Delete within same line.
1264 auto& line = this->line(range.start().line());
1265 if (line.length() == 0)
1266 return;
1267
1268 bool whole_line_is_selected = range.start().column() == 0 && range.end().column() == line.length();
1269
1270 if (whole_line_is_selected) {
1271 line.clear(*this);
1272 } else {
1273 line.remove_range(*this, range.start().column(), range.end().column() - range.start().column());
1274 }
1275 } else {
1276 // Delete across a newline, merging lines.
1277 VERIFY(range.start().line() == range.end().line() - 1);
1278
1279 auto& first_line = line(range.start().line());
1280 auto& second_line = line(range.end().line());
1281
1282 Vector<u32> code_points;
1283 code_points.append(first_line.code_points(), range.start().column());
1284 if (!second_line.is_empty())
1285 code_points.append(second_line.code_points() + range.end().column(), second_line.length() - range.end().column());
1286 first_line.set_text(*this, move(code_points));
1287
1288 remove_line(range.end().line());
1289 }
1290
1291 if (lines().is_empty()) {
1292 append_line(make<TextDocumentLine>(*this));
1293 }
1294
1295 notify_did_change();
1296}
1297
1298bool TextDocument::is_empty() const
1299{
1300 return line_count() == 1 && line(0).is_empty();
1301}
1302
1303TextRange TextDocument::range_for_entire_line(size_t line_index) const
1304{
1305 if (line_index >= line_count())
1306 return {};
1307 return { { line_index, 0 }, { line_index, line(line_index).length() } };
1308}
1309
1310TextDocumentSpan const* TextDocument::span_at(TextPosition const& position) const
1311{
1312 for (auto& span : m_spans) {
1313 if (span.range.contains(position))
1314 return &span;
1315 }
1316 return nullptr;
1317}
1318
1319void TextDocument::set_unmodified()
1320{
1321 m_undo_stack.set_current_unmodified();
1322}
1323
1324void TextDocument::set_spans(u32 span_collection_index, Vector<TextDocumentSpan> spans)
1325{
1326 m_span_collections.set(span_collection_index, move(spans));
1327 merge_span_collections();
1328}
1329
1330struct SpanAndCollectionIndex {
1331 TextDocumentSpan span;
1332 u32 collection_index { 0 };
1333};
1334
1335void TextDocument::merge_span_collections()
1336{
1337 Vector<SpanAndCollectionIndex> sorted_spans;
1338 auto collection_indices = m_span_collections.keys();
1339 quick_sort(collection_indices);
1340
1341 for (auto collection_index : collection_indices) {
1342 auto spans = m_span_collections.get(collection_index).value();
1343 for (auto span : spans) {
1344 sorted_spans.append({ move(span), collection_index });
1345 }
1346 }
1347
1348 quick_sort(sorted_spans, [](SpanAndCollectionIndex const& a, SpanAndCollectionIndex const& b) {
1349 if (a.span.range.start() == b.span.range.start()) {
1350 return a.collection_index < b.collection_index;
1351 }
1352 return a.span.range.start() < b.span.range.start();
1353 });
1354
1355 // The end of the TextRanges of spans are non-inclusive, i.e span range = [X,y).
1356 // This transforms the span's range to be inclusive, i.e [X,Y].
1357 auto adjust_end = [](GUI::TextDocumentSpan span) -> GUI::TextDocumentSpan {
1358 span.range.set_end({ span.range.end().line(), span.range.end().column() == 0 ? 0 : span.range.end().column() - 1 });
1359 return span;
1360 };
1361
1362 Vector<SpanAndCollectionIndex> merged_spans;
1363 for (auto& span_and_collection_index : sorted_spans) {
1364 if (merged_spans.is_empty()) {
1365 merged_spans.append(span_and_collection_index);
1366 continue;
1367 }
1368
1369 auto const& span = span_and_collection_index.span;
1370 auto last_span_and_collection_index = merged_spans.last();
1371 auto const& last_span = last_span_and_collection_index.span;
1372
1373 if (adjust_end(span).range.start() > adjust_end(last_span).range.end()) {
1374 // Current span does not intersect with previous one, can simply append to merged list.
1375 merged_spans.append(span_and_collection_index);
1376 continue;
1377 }
1378 merged_spans.take_last();
1379
1380 if (span.range.start() > last_span.range.start()) {
1381 SpanAndCollectionIndex first_part = last_span_and_collection_index;
1382 first_part.span.range.set_end(span.range.start());
1383 merged_spans.append(move(first_part));
1384 }
1385
1386 SpanAndCollectionIndex merged_span;
1387 merged_span.collection_index = span_and_collection_index.collection_index;
1388 merged_span.span.range = { span.range.start(), min(span.range.end(), last_span.range.end()) };
1389 merged_span.span.is_skippable = span.is_skippable | last_span.is_skippable;
1390 merged_span.span.data = span.data ? span.data : last_span.data;
1391 merged_span.span.attributes.color = span_and_collection_index.collection_index > last_span_and_collection_index.collection_index ? span.attributes.color : last_span.attributes.color;
1392 merged_span.span.attributes.bold = span.attributes.bold | last_span.attributes.bold;
1393 merged_span.span.attributes.background_color = span.attributes.background_color.has_value() ? span.attributes.background_color.value() : last_span.attributes.background_color;
1394 merged_span.span.attributes.underline = span.attributes.underline | last_span.attributes.underline;
1395 merged_span.span.attributes.underline_color = span.attributes.underline_color.has_value() ? span.attributes.underline_color.value() : last_span.attributes.underline_color;
1396 merged_span.span.attributes.underline_style = span.attributes.underline_style;
1397 merged_spans.append(move(merged_span));
1398
1399 if (span.range.end() == last_span.range.end())
1400 continue;
1401
1402 if (span.range.end() > last_span.range.end()) {
1403 SpanAndCollectionIndex last_part = span_and_collection_index;
1404 last_part.span.range.set_start(last_span.range.end());
1405 merged_spans.append(move(last_part));
1406 continue;
1407 }
1408
1409 SpanAndCollectionIndex last_part = last_span_and_collection_index;
1410 last_part.span.range.set_start(span.range.end());
1411 merged_spans.append(move(last_part));
1412 }
1413
1414 m_spans.clear();
1415 TextDocumentSpan previous_span { .range = { TextPosition(0, 0), TextPosition(0, 0) }, .attributes = {} };
1416 for (auto span : merged_spans) {
1417 // Validate spans
1418 if (!span.span.range.is_valid()) {
1419 dbgln_if(TEXTEDITOR_DEBUG, "Invalid span {} => ignoring", span.span.range);
1420 continue;
1421 }
1422 if (span.span.range.end() < span.span.range.start()) {
1423 dbgln_if(TEXTEDITOR_DEBUG, "Span {} has negative length => ignoring", span.span.range);
1424 continue;
1425 }
1426 if (span.span.range.end() < previous_span.range.start()) {
1427 dbgln_if(TEXTEDITOR_DEBUG, "Spans not sorted (Span {} ends before previous span {}) => ignoring", span.span.range, previous_span.range);
1428 continue;
1429 }
1430 if (span.span.range.start() < previous_span.range.end()) {
1431 dbgln_if(TEXTEDITOR_DEBUG, "Span {} overlaps previous span {} => ignoring", span.span.range, previous_span.range);
1432 continue;
1433 }
1434
1435 previous_span = span.span;
1436 m_spans.append(move(span.span));
1437 }
1438}
1439
1440void TextDocument::set_folding_regions(Vector<TextDocumentFoldingRegion> folding_regions)
1441{
1442 // Remove any regions that don't span at least 3 lines.
1443 // Currently, we can't do anything useful with them, and our implementation gets very confused by
1444 // single-line regions, so drop them.
1445 folding_regions.remove_all_matching([](TextDocumentFoldingRegion const& region) {
1446 return region.range.line_count() < 3;
1447 });
1448
1449 quick_sort(folding_regions, [](TextDocumentFoldingRegion const& a, TextDocumentFoldingRegion const& b) {
1450 return a.range.start() < b.range.start();
1451 });
1452
1453 for (auto& folding_region : folding_regions) {
1454 folding_region.line_ptr = &line(folding_region.range.start().line());
1455
1456 // Map the new folding region to an old one, to preserve which regions were folded.
1457 // FIXME: This is O(n*n).
1458 for (auto const& existing_folding_region : m_folding_regions) {
1459 // We treat two folding regions as the same if they start on the same TextDocumentLine,
1460 // and have the same line count. The actual line *numbers* might change, but the pointer
1461 // and count should not.
1462 if (existing_folding_region.line_ptr
1463 && existing_folding_region.line_ptr == folding_region.line_ptr
1464 && existing_folding_region.range.line_count() == folding_region.range.line_count()) {
1465 folding_region.is_folded = existing_folding_region.is_folded;
1466 break;
1467 }
1468 }
1469 }
1470
1471 // FIXME: Remove any regions that partially overlap another region, since these are invalid.
1472
1473 m_folding_regions = move(folding_regions);
1474
1475 if constexpr (TEXTEDITOR_DEBUG) {
1476 dbgln("TextDocument got {} fold regions:", m_folding_regions.size());
1477 for (auto const& item : m_folding_regions) {
1478 dbgln("- {} (ptr: {:p}, folded: {})", item.range, item.line_ptr, item.is_folded);
1479 }
1480 }
1481}
1482
1483Optional<TextDocumentFoldingRegion&> TextDocument::folding_region_starting_on_line(size_t line)
1484{
1485 return m_folding_regions.first_matching([line](auto& region) {
1486 return region.range.start().line() == line;
1487 });
1488}
1489
1490bool TextDocument::line_is_visible(size_t line) const
1491{
1492 // FIXME: line_is_visible() gets called a lot.
1493 // We could avoid a lot of repeated work if we saved this state on the TextDocumentLine.
1494 return !any_of(m_folding_regions, [line](auto& region) {
1495 return region.is_folded
1496 && line > region.range.start().line()
1497 && line < region.range.end().line();
1498 });
1499}
1500
1501Vector<TextDocumentFoldingRegion const&> TextDocument::currently_folded_regions() const
1502{
1503 Vector<TextDocumentFoldingRegion const&> folded_regions;
1504
1505 for (auto& region : m_folding_regions) {
1506 if (region.is_folded) {
1507 // Only add this region if it's not contained within a previous folded region.
1508 // Because regions are sorted by their start position, and regions cannot partially overlap,
1509 // we can just see if it starts inside the last region we appended.
1510 if (!folded_regions.is_empty() && folded_regions.last().range.contains(region.range.start()))
1511 continue;
1512
1513 folded_regions.append(region);
1514 }
1515 }
1516
1517 return folded_regions;
1518}
1519
1520}