Serenity Operating System
at hosted 602 lines 19 kB view raw
1/* 2 * Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org> 3 * All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions are met: 7 * 8 * 1. Redistributions of source code must retain the above copyright notice, this 9 * list of conditions and the following disclaimer. 10 * 11 * 2. Redistributions in binary form must reproduce the above copyright notice, 12 * this list of conditions and the following disclaimer in the documentation 13 * and/or other materials provided with the distribution. 14 * 15 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 */ 26 27#include <AK/Badge.h> 28#include <AK/StringBuilder.h> 29#include <LibCore/Timer.h> 30#include <LibGUI/TextDocument.h> 31#include <LibGUI/TextEditor.h> 32#include <ctype.h> 33 34namespace GUI { 35 36NonnullRefPtr<TextDocument> TextDocument::create(Client* client) 37{ 38 return adopt(*new TextDocument(client)); 39} 40 41TextDocument::TextDocument(Client* client) 42{ 43 if (client) 44 m_clients.set(client); 45 append_line(make<TextDocumentLine>(*this)); 46 47 // TODO: Instead of a repating timer, this we should call a delayed 2 sec timer when the user types. 48 m_undo_timer = Core::Timer::construct( 49 2000, [this] { 50 update_undo_timer(); 51 }); 52} 53 54TextDocument::~TextDocument() 55{ 56} 57 58void TextDocument::set_text(const StringView& text) 59{ 60 m_client_notifications_enabled = false; 61 m_spans.clear(); 62 remove_all_lines(); 63 64 size_t start_of_current_line = 0; 65 66 auto add_line = [&](size_t current_position) { 67 size_t line_length = current_position - start_of_current_line; 68 auto line = make<TextDocumentLine>(*this); 69 if (line_length) 70 line->set_text(*this, text.substring_view(start_of_current_line, current_position - start_of_current_line)); 71 append_line(move(line)); 72 start_of_current_line = current_position + 1; 73 }; 74 size_t i = 0; 75 for (i = 0; i < text.length(); ++i) { 76 if (text[i] == '\n') 77 add_line(i); 78 } 79 add_line(i); 80 m_client_notifications_enabled = true; 81 82 for (auto* client : m_clients) 83 client->document_did_set_text(); 84} 85 86size_t TextDocumentLine::first_non_whitespace_column() const 87{ 88 for (size_t i = 0; i < length(); ++i) { 89 if (!isspace(m_text[i])) 90 return i; 91 } 92 return length(); 93} 94 95TextDocumentLine::TextDocumentLine(TextDocument& document) 96{ 97 clear(document); 98} 99 100TextDocumentLine::TextDocumentLine(TextDocument& document, const StringView& text) 101{ 102 set_text(document, text); 103} 104 105void TextDocumentLine::clear(TextDocument& document) 106{ 107 m_text.clear(); 108 m_text.append(0); 109 document.update_views({}); 110} 111 112void TextDocumentLine::set_text(TextDocument& document, const StringView& text) 113{ 114 if (text.length() == length() && !memcmp(text.characters_without_null_termination(), characters(), length())) 115 return; 116 if (text.is_empty()) { 117 clear(document); 118 return; 119 } 120 m_text.resize((int)text.length() + 1); 121 memcpy(m_text.data(), text.characters_without_null_termination(), text.length()); 122 m_text.last() = 0; 123 document.update_views({}); 124} 125 126void TextDocumentLine::append(TextDocument& document, const char* characters, size_t length) 127{ 128 int old_length = m_text.size() - 1; 129 m_text.resize(m_text.size() + length); 130 memcpy(m_text.data() + old_length, characters, length); 131 m_text.last() = 0; 132 document.update_views({}); 133} 134 135void TextDocumentLine::append(TextDocument& document, char ch) 136{ 137 insert(document, length(), ch); 138} 139 140void TextDocumentLine::prepend(TextDocument& document, char ch) 141{ 142 insert(document, 0, ch); 143} 144 145void TextDocumentLine::insert(TextDocument& document, size_t index, char ch) 146{ 147 if (index == length()) { 148 m_text.last() = ch; 149 m_text.append(0); 150 } else { 151 m_text.insert((int)index, move(ch)); 152 } 153 document.update_views({}); 154} 155 156void TextDocumentLine::remove(TextDocument& document, size_t index) 157{ 158 if (index == length()) { 159 m_text.take_last(); 160 m_text.last() = 0; 161 } else { 162 m_text.remove((int)index); 163 } 164 document.update_views({}); 165} 166 167void TextDocumentLine::truncate(TextDocument& document, size_t length) 168{ 169 m_text.resize((int)length + 1); 170 m_text.last() = 0; 171 document.update_views({}); 172} 173 174void TextDocument::append_line(NonnullOwnPtr<TextDocumentLine> line) 175{ 176 lines().append(move(line)); 177 if (m_client_notifications_enabled) { 178 for (auto* client : m_clients) 179 client->document_did_append_line(); 180 } 181} 182 183void TextDocument::insert_line(size_t line_index, NonnullOwnPtr<TextDocumentLine> line) 184{ 185 lines().insert((int)line_index, move(line)); 186 if (m_client_notifications_enabled) { 187 for (auto* client : m_clients) 188 client->document_did_insert_line(line_index); 189 } 190} 191 192void TextDocument::remove_line(size_t line_index) 193{ 194 lines().remove((int)line_index); 195 if (m_client_notifications_enabled) { 196 for (auto* client : m_clients) 197 client->document_did_remove_line(line_index); 198 } 199} 200 201void TextDocument::remove_all_lines() 202{ 203 lines().clear(); 204 if (m_client_notifications_enabled) { 205 for (auto* client : m_clients) 206 client->document_did_remove_all_lines(); 207 } 208} 209 210TextDocument::Client::~Client() 211{ 212} 213 214void TextDocument::register_client(Client& client) 215{ 216 m_clients.set(&client); 217} 218 219void TextDocument::unregister_client(Client& client) 220{ 221 m_clients.remove(&client); 222} 223 224void TextDocument::update_views(Badge<TextDocumentLine>) 225{ 226 notify_did_change(); 227} 228 229void TextDocument::notify_did_change() 230{ 231 if (m_client_notifications_enabled) { 232 for (auto* client : m_clients) 233 client->document_did_change(); 234 } 235} 236 237void TextDocument::set_all_cursors(const TextPosition& position) 238{ 239 if (m_client_notifications_enabled) { 240 for (auto* client : m_clients) 241 client->document_did_set_cursor(position); 242 } 243} 244 245String TextDocument::text_in_range(const TextRange& a_range) const 246{ 247 auto range = a_range.normalized(); 248 249 StringBuilder builder; 250 for (size_t i = range.start().line(); i <= range.end().line(); ++i) { 251 auto& line = this->line(i); 252 size_t selection_start_column_on_line = range.start().line() == i ? range.start().column() : 0; 253 size_t selection_end_column_on_line = range.end().line() == i ? range.end().column() : line.length(); 254 builder.append(line.characters() + selection_start_column_on_line, selection_end_column_on_line - selection_start_column_on_line); 255 if (i != range.end().line()) 256 builder.append('\n'); 257 } 258 259 return builder.to_string(); 260} 261 262char TextDocument::character_at(const TextPosition& position) const 263{ 264 ASSERT(position.line() < line_count()); 265 auto& line = this->line(position.line()); 266 if (position.column() == line.length()) 267 return '\n'; 268 return line.characters()[position.column()]; 269} 270 271TextPosition TextDocument::next_position_after(const TextPosition& position, SearchShouldWrap should_wrap) const 272{ 273 auto& line = this->line(position.line()); 274 if (position.column() == line.length()) { 275 if (position.line() == line_count() - 1) { 276 if (should_wrap == SearchShouldWrap::Yes) 277 return { 0, 0 }; 278 return {}; 279 } 280 return { position.line() + 1, 0 }; 281 } 282 return { position.line(), position.column() + 1 }; 283} 284 285TextPosition TextDocument::previous_position_before(const TextPosition& position, SearchShouldWrap should_wrap) const 286{ 287 if (position.column() == 0) { 288 if (position.line() == 0) { 289 if (should_wrap == SearchShouldWrap::Yes) { 290 auto& last_line = this->line(line_count() - 1); 291 return { line_count() - 1, last_line.length() }; 292 } 293 return {}; 294 } 295 auto& prev_line = this->line(position.line() - 1); 296 return { position.line() - 1, prev_line.length() }; 297 } 298 return { position.line(), position.column() - 1 }; 299} 300 301TextRange TextDocument::find_next(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap) const 302{ 303 if (needle.is_empty()) 304 return {}; 305 306 TextPosition position = start.is_valid() ? start : TextPosition(0, 0); 307 TextPosition original_position = position; 308 309 TextPosition start_of_potential_match; 310 size_t needle_index = 0; 311 312 do { 313 auto ch = character_at(position); 314 if (ch == needle[needle_index]) { 315 if (needle_index == 0) 316 start_of_potential_match = position; 317 ++needle_index; 318 if (needle_index >= needle.length()) 319 return { start_of_potential_match, next_position_after(position, should_wrap) }; 320 } else { 321 if (needle_index > 0) 322 position = start_of_potential_match; 323 needle_index = 0; 324 } 325 position = next_position_after(position, should_wrap); 326 } while (position.is_valid() && position != original_position); 327 328 return {}; 329} 330 331TextRange TextDocument::find_previous(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap) const 332{ 333 if (needle.is_empty()) 334 return {}; 335 336 TextPosition position = start.is_valid() ? start : TextPosition(0, 0); 337 position = previous_position_before(position, should_wrap); 338 TextPosition original_position = position; 339 340 TextPosition end_of_potential_match; 341 size_t needle_index = needle.length() - 1; 342 343 do { 344 auto ch = character_at(position); 345 if (ch == needle[needle_index]) { 346 if (needle_index == needle.length() - 1) 347 end_of_potential_match = position; 348 if (needle_index == 0) 349 return { position, next_position_after(end_of_potential_match, should_wrap) }; 350 --needle_index; 351 } else { 352 if (needle_index < needle.length() - 1) 353 position = end_of_potential_match; 354 needle_index = needle.length() - 1; 355 } 356 position = previous_position_before(position, should_wrap); 357 } while (position.is_valid() && position != original_position); 358 359 return {}; 360} 361 362Vector<TextRange> TextDocument::find_all(const StringView& needle) const 363{ 364 Vector<TextRange> ranges; 365 366 TextPosition position; 367 for (;;) { 368 auto range = find_next(needle, position, SearchShouldWrap::No); 369 if (!range.is_valid()) 370 break; 371 ranges.append(range); 372 position = range.end(); 373 } 374 return ranges; 375} 376 377Optional<TextDocumentSpan> TextDocument::first_non_skippable_span_before(const TextPosition& position) const 378{ 379 for (int i = m_spans.size() - 1; i >= 0; --i) { 380 if (!m_spans[i].range.contains(position)) 381 continue; 382 while ((i - 1) >= 0 && m_spans[i - 1].is_skippable) 383 --i; 384 if (i <= 0) 385 return {}; 386 return m_spans[i - 1]; 387 } 388 return {}; 389} 390 391Optional<TextDocumentSpan> TextDocument::first_non_skippable_span_after(const TextPosition& position) const 392{ 393 for (size_t i = 0; i < m_spans.size(); ++i) { 394 if (!m_spans[i].range.contains(position)) 395 continue; 396 while ((i + 1) < m_spans.size() && m_spans[i + 1].is_skippable) 397 ++i; 398 if (i >= (m_spans.size() - 1)) 399 return {}; 400 return m_spans[i + 1]; 401 } 402 return {}; 403} 404 405void TextDocument::undo() 406{ 407 if (!can_undo()) 408 return; 409 m_undo_stack.undo(); 410 notify_did_change(); 411} 412 413void TextDocument::redo() 414{ 415 if (!can_redo()) 416 return; 417 m_undo_stack.redo(); 418 notify_did_change(); 419} 420 421void TextDocument::add_to_undo_stack(NonnullOwnPtr<TextDocumentUndoCommand> undo_command) 422{ 423 m_undo_stack.push(move(undo_command)); 424} 425 426TextDocumentUndoCommand::TextDocumentUndoCommand(TextDocument& document) 427 : m_document(document) 428{ 429} 430 431TextDocumentUndoCommand::~TextDocumentUndoCommand() 432{ 433} 434 435InsertTextCommand::InsertTextCommand(TextDocument& document, const String& text, const TextPosition& position) 436 : TextDocumentUndoCommand(document) 437 , m_text(text) 438 , m_range({ position, position }) 439{ 440} 441 442void InsertTextCommand::redo() 443{ 444 auto new_cursor = m_document.insert_at(m_range.start(), m_text, m_client); 445 // NOTE: We don't know where the range ends until after doing redo(). 446 // This is okay since we always do redo() after adding this to the undo stack. 447 m_range.set_end(new_cursor); 448 m_document.set_all_cursors(new_cursor); 449} 450 451void InsertTextCommand::undo() 452{ 453 m_document.remove(m_range); 454 m_document.set_all_cursors(m_range.start()); 455} 456 457RemoveTextCommand::RemoveTextCommand(TextDocument& document, const String& text, const TextRange& range) 458 : TextDocumentUndoCommand(document) 459 , m_text(text) 460 , m_range(range) 461{ 462} 463 464void RemoveTextCommand::redo() 465{ 466 m_document.remove(m_range); 467 m_document.set_all_cursors(m_range.start()); 468} 469 470void RemoveTextCommand::undo() 471{ 472 auto new_cursor = m_document.insert_at(m_range.start(), m_text); 473 m_document.set_all_cursors(new_cursor); 474} 475 476void TextDocument::update_undo_timer() 477{ 478 m_undo_stack.finalize_current_combo(); 479} 480 481TextPosition TextDocument::insert_at(const TextPosition& position, const StringView& text, const Client* client) 482{ 483 TextPosition cursor = position; 484 for (size_t i = 0; i < text.length(); ++i) 485 cursor = insert_at(cursor, text[i], client); 486 return cursor; 487} 488 489TextPosition TextDocument::insert_at(const TextPosition& position, char ch, const Client* client) 490{ 491 bool automatic_indentation_enabled = client ? client->is_automatic_indentation_enabled() : false; 492 size_t m_soft_tab_width = client ? client->soft_tab_width() : 4; 493 494 bool at_head = position.column() == 0; 495 bool at_tail = position.column() == line(position.line()).length(); 496 if (ch == '\n') { 497 if (at_tail || at_head) { 498 String new_line_contents; 499 if (automatic_indentation_enabled && at_tail) { 500 size_t leading_spaces = 0; 501 auto& old_line = lines()[position.line()]; 502 for (size_t i = 0; i < old_line.length(); ++i) { 503 if (old_line.characters()[i] == ' ') 504 ++leading_spaces; 505 else 506 break; 507 } 508 if (leading_spaces) 509 new_line_contents = String::repeated(' ', leading_spaces); 510 } 511 512 size_t row = position.line(); 513 Vector<char> line_content; 514 for (size_t i = position.column(); i < line(row).length(); i++) 515 line_content.append(line(row).characters()[i]); 516 insert_line(position.line() + (at_tail ? 1 : 0), make<TextDocumentLine>(*this, new_line_contents)); 517 notify_did_change(); 518 return { position.line() + 1, line(position.line() + 1).length() }; 519 } 520 auto new_line = make<TextDocumentLine>(*this); 521 new_line->append(*this, line(position.line()).characters() + position.column(), line(position.line()).length() - position.column()); 522 523 Vector<char> line_content; 524 for (size_t i = 0; i < new_line->length(); i++) 525 line_content.append(new_line->characters()[i]); 526 line(position.line()).truncate(*this, position.column()); 527 insert_line(position.line() + 1, move(new_line)); 528 notify_did_change(); 529 return { position.line() + 1, 0 }; 530 } 531 if (ch == '\t') { 532 size_t next_soft_tab_stop = ((position.column() + m_soft_tab_width) / m_soft_tab_width) * m_soft_tab_width; 533 size_t spaces_to_insert = next_soft_tab_stop - position.column(); 534 for (size_t i = 0; i < spaces_to_insert; ++i) { 535 line(position.line()).insert(*this, position.column(), ' '); 536 } 537 notify_did_change(); 538 return { position.line(), next_soft_tab_stop }; 539 } 540 line(position.line()).insert(*this, position.column(), ch); 541 notify_did_change(); 542 return { position.line(), position.column() + 1 }; 543} 544 545void TextDocument::remove(const TextRange& unnormalized_range) 546{ 547 if (!unnormalized_range.is_valid()) 548 return; 549 550 auto range = unnormalized_range.normalized(); 551 552 // First delete all the lines in between the first and last one. 553 for (size_t i = range.start().line() + 1; i < range.end().line();) { 554 remove_line(i); 555 range.end().set_line(range.end().line() - 1); 556 } 557 558 if (range.start().line() == range.end().line()) { 559 // Delete within same line. 560 auto& line = this->line(range.start().line()); 561 bool whole_line_is_selected = range.start().column() == 0 && range.end().column() == line.length(); 562 563 if (whole_line_is_selected) { 564 line.clear(*this); 565 } else { 566 auto before_selection = String(line.characters(), line.length()).substring(0, range.start().column()); 567 auto after_selection = String(line.characters(), line.length()).substring(range.end().column(), line.length() - range.end().column()); 568 StringBuilder builder(before_selection.length() + after_selection.length()); 569 builder.append(before_selection); 570 builder.append(after_selection); 571 line.set_text(*this, builder.to_string()); 572 } 573 } else { 574 // Delete across a newline, merging lines. 575 ASSERT(range.start().line() == range.end().line() - 1); 576 auto& first_line = line(range.start().line()); 577 auto& second_line = line(range.end().line()); 578 auto before_selection = String(first_line.characters(), first_line.length()).substring(0, range.start().column()); 579 auto after_selection = String(second_line.characters(), second_line.length()).substring(range.end().column(), second_line.length() - range.end().column()); 580 StringBuilder builder(before_selection.length() + after_selection.length()); 581 builder.append(before_selection); 582 builder.append(after_selection); 583 584 first_line.set_text(*this, builder.to_string()); 585 remove_line(range.end().line()); 586 } 587 588 if (lines().is_empty()) { 589 append_line(make<TextDocumentLine>(*this)); 590 } 591 592 notify_did_change(); 593} 594 595TextRange TextDocument::range_for_entire_line(size_t line_index) const 596{ 597 if (line_index >= line_count()) 598 return {}; 599 return { { line_index, 0 }, { line_index, line(line_index).length() } }; 600} 601 602}