Serenity Operating System
at master 1520 lines 50 kB view raw
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}