Serenity Operating System
at portability 601 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[(int)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() + 1); 122 document.update_views({}); 123} 124 125void TextDocumentLine::append(TextDocument& document, const char* characters, size_t length) 126{ 127 int old_length = m_text.size() - 1; 128 m_text.resize(m_text.size() + length); 129 memcpy(m_text.data() + old_length, characters, length); 130 m_text.last() = 0; 131 document.update_views({}); 132} 133 134void TextDocumentLine::append(TextDocument& document, char ch) 135{ 136 insert(document, length(), ch); 137} 138 139void TextDocumentLine::prepend(TextDocument& document, char ch) 140{ 141 insert(document, 0, ch); 142} 143 144void TextDocumentLine::insert(TextDocument& document, size_t index, char ch) 145{ 146 if (index == length()) { 147 m_text.last() = ch; 148 m_text.append(0); 149 } else { 150 m_text.insert((int)index, move(ch)); 151 } 152 document.update_views({}); 153} 154 155void TextDocumentLine::remove(TextDocument& document, size_t index) 156{ 157 if (index == length()) { 158 m_text.take_last(); 159 m_text.last() = 0; 160 } else { 161 m_text.remove((int)index); 162 } 163 document.update_views({}); 164} 165 166void TextDocumentLine::truncate(TextDocument& document, size_t length) 167{ 168 m_text.resize((int)length + 1); 169 m_text.last() = 0; 170 document.update_views({}); 171} 172 173void TextDocument::append_line(NonnullOwnPtr<TextDocumentLine> line) 174{ 175 lines().append(move(line)); 176 if (m_client_notifications_enabled) { 177 for (auto* client : m_clients) 178 client->document_did_append_line(); 179 } 180} 181 182void TextDocument::insert_line(size_t line_index, NonnullOwnPtr<TextDocumentLine> line) 183{ 184 lines().insert((int)line_index, move(line)); 185 if (m_client_notifications_enabled) { 186 for (auto* client : m_clients) 187 client->document_did_insert_line(line_index); 188 } 189} 190 191void TextDocument::remove_line(size_t line_index) 192{ 193 lines().remove((int)line_index); 194 if (m_client_notifications_enabled) { 195 for (auto* client : m_clients) 196 client->document_did_remove_line(line_index); 197 } 198} 199 200void TextDocument::remove_all_lines() 201{ 202 lines().clear(); 203 if (m_client_notifications_enabled) { 204 for (auto* client : m_clients) 205 client->document_did_remove_all_lines(); 206 } 207} 208 209TextDocument::Client::~Client() 210{ 211} 212 213void TextDocument::register_client(Client& client) 214{ 215 m_clients.set(&client); 216} 217 218void TextDocument::unregister_client(Client& client) 219{ 220 m_clients.remove(&client); 221} 222 223void TextDocument::update_views(Badge<TextDocumentLine>) 224{ 225 notify_did_change(); 226} 227 228void TextDocument::notify_did_change() 229{ 230 if (m_client_notifications_enabled) { 231 for (auto* client : m_clients) 232 client->document_did_change(); 233 } 234} 235 236void TextDocument::set_all_cursors(const TextPosition& position) 237{ 238 if (m_client_notifications_enabled) { 239 for (auto* client : m_clients) 240 client->document_did_set_cursor(position); 241 } 242} 243 244String TextDocument::text_in_range(const TextRange& a_range) const 245{ 246 auto range = a_range.normalized(); 247 248 StringBuilder builder; 249 for (size_t i = range.start().line(); i <= range.end().line(); ++i) { 250 auto& line = this->line(i); 251 size_t selection_start_column_on_line = range.start().line() == i ? range.start().column() : 0; 252 size_t selection_end_column_on_line = range.end().line() == i ? range.end().column() : line.length(); 253 builder.append(line.characters() + selection_start_column_on_line, selection_end_column_on_line - selection_start_column_on_line); 254 if (i != range.end().line()) 255 builder.append('\n'); 256 } 257 258 return builder.to_string(); 259} 260 261char TextDocument::character_at(const TextPosition& position) const 262{ 263 ASSERT(position.line() < line_count()); 264 auto& line = this->line(position.line()); 265 if (position.column() == line.length()) 266 return '\n'; 267 return line.characters()[position.column()]; 268} 269 270TextPosition TextDocument::next_position_after(const TextPosition& position, SearchShouldWrap should_wrap) const 271{ 272 auto& line = this->line(position.line()); 273 if (position.column() == line.length()) { 274 if (position.line() == line_count() - 1) { 275 if (should_wrap == SearchShouldWrap::Yes) 276 return { 0, 0 }; 277 return {}; 278 } 279 return { position.line() + 1, 0 }; 280 } 281 return { position.line(), position.column() + 1 }; 282} 283 284TextPosition TextDocument::previous_position_before(const TextPosition& position, SearchShouldWrap should_wrap) const 285{ 286 if (position.column() == 0) { 287 if (position.line() == 0) { 288 if (should_wrap == SearchShouldWrap::Yes) { 289 auto& last_line = this->line(line_count() - 1); 290 return { line_count() - 1, last_line.length() }; 291 } 292 return {}; 293 } 294 auto& prev_line = this->line(position.line() - 1); 295 return { position.line() - 1, prev_line.length() }; 296 } 297 return { position.line(), position.column() - 1 }; 298} 299 300TextRange TextDocument::find_next(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap) const 301{ 302 if (needle.is_empty()) 303 return {}; 304 305 TextPosition position = start.is_valid() ? start : TextPosition(0, 0); 306 TextPosition original_position = position; 307 308 TextPosition start_of_potential_match; 309 size_t needle_index = 0; 310 311 do { 312 auto ch = character_at(position); 313 if (ch == needle[needle_index]) { 314 if (needle_index == 0) 315 start_of_potential_match = position; 316 ++needle_index; 317 if (needle_index >= needle.length()) 318 return { start_of_potential_match, next_position_after(position, should_wrap) }; 319 } else { 320 if (needle_index > 0) 321 position = start_of_potential_match; 322 needle_index = 0; 323 } 324 position = next_position_after(position, should_wrap); 325 } while (position.is_valid() && position != original_position); 326 327 return {}; 328} 329 330TextRange TextDocument::find_previous(const StringView& needle, const TextPosition& start, SearchShouldWrap should_wrap) const 331{ 332 if (needle.is_empty()) 333 return {}; 334 335 TextPosition position = start.is_valid() ? start : TextPosition(0, 0); 336 position = previous_position_before(position, should_wrap); 337 TextPosition original_position = position; 338 339 TextPosition end_of_potential_match; 340 size_t needle_index = needle.length() - 1; 341 342 do { 343 auto ch = character_at(position); 344 if (ch == needle[needle_index]) { 345 if (needle_index == needle.length() - 1) 346 end_of_potential_match = position; 347 if (needle_index == 0) 348 return { position, next_position_after(end_of_potential_match, should_wrap) }; 349 --needle_index; 350 } else { 351 if (needle_index < needle.length() - 1) 352 position = end_of_potential_match; 353 needle_index = needle.length() - 1; 354 } 355 position = previous_position_before(position, should_wrap); 356 } while (position.is_valid() && position != original_position); 357 358 return {}; 359} 360 361Vector<TextRange> TextDocument::find_all(const StringView& needle) const 362{ 363 Vector<TextRange> ranges; 364 365 TextPosition position; 366 for (;;) { 367 auto range = find_next(needle, position, SearchShouldWrap::No); 368 if (!range.is_valid()) 369 break; 370 ranges.append(range); 371 position = range.end(); 372 } 373 return ranges; 374} 375 376Optional<TextDocumentSpan> TextDocument::first_non_skippable_span_before(const TextPosition& position) const 377{ 378 for (int i = m_spans.size() - 1; i >= 0; --i) { 379 if (!m_spans[i].range.contains(position)) 380 continue; 381 while ((i - 1) >= 0 && m_spans[i - 1].is_skippable) 382 --i; 383 if (i <= 0) 384 return {}; 385 return m_spans[i - 1]; 386 } 387 return {}; 388} 389 390Optional<TextDocumentSpan> TextDocument::first_non_skippable_span_after(const TextPosition& position) const 391{ 392 for (size_t i = 0; i < m_spans.size(); ++i) { 393 if (!m_spans[i].range.contains(position)) 394 continue; 395 while ((i + 1) < m_spans.size() && m_spans[i + 1].is_skippable) 396 ++i; 397 if (i >= (m_spans.size() - 1)) 398 return {}; 399 return m_spans[i + 1]; 400 } 401 return {}; 402} 403 404void TextDocument::undo() 405{ 406 if (!can_undo()) 407 return; 408 m_undo_stack.undo(); 409 notify_did_change(); 410} 411 412void TextDocument::redo() 413{ 414 if (!can_redo()) 415 return; 416 m_undo_stack.redo(); 417 notify_did_change(); 418} 419 420void TextDocument::add_to_undo_stack(NonnullOwnPtr<TextDocumentUndoCommand> undo_command) 421{ 422 m_undo_stack.push(move(undo_command)); 423} 424 425TextDocumentUndoCommand::TextDocumentUndoCommand(TextDocument& document) 426 : m_document(document) 427{ 428} 429 430TextDocumentUndoCommand::~TextDocumentUndoCommand() 431{ 432} 433 434InsertTextCommand::InsertTextCommand(TextDocument& document, const String& text, const TextPosition& position) 435 : TextDocumentUndoCommand(document) 436 , m_text(text) 437 , m_range({ position, position }) 438{ 439} 440 441void InsertTextCommand::redo() 442{ 443 auto new_cursor = m_document.insert_at(m_range.start(), m_text, m_client); 444 // NOTE: We don't know where the range ends until after doing redo(). 445 // This is okay since we always do redo() after adding this to the undo stack. 446 m_range.set_end(new_cursor); 447 m_document.set_all_cursors(new_cursor); 448} 449 450void InsertTextCommand::undo() 451{ 452 m_document.remove(m_range); 453 m_document.set_all_cursors(m_range.start()); 454} 455 456RemoveTextCommand::RemoveTextCommand(TextDocument& document, const String& text, const TextRange& range) 457 : TextDocumentUndoCommand(document) 458 , m_text(text) 459 , m_range(range) 460{ 461} 462 463void RemoveTextCommand::redo() 464{ 465 m_document.remove(m_range); 466 m_document.set_all_cursors(m_range.start()); 467} 468 469void RemoveTextCommand::undo() 470{ 471 auto new_cursor = m_document.insert_at(m_range.start(), m_text); 472 m_document.set_all_cursors(new_cursor); 473} 474 475void TextDocument::update_undo_timer() 476{ 477 m_undo_stack.finalize_current_combo(); 478} 479 480TextPosition TextDocument::insert_at(const TextPosition& position, const StringView& text, const Client* client) 481{ 482 TextPosition cursor = position; 483 for (size_t i = 0; i < text.length(); ++i) 484 cursor = insert_at(cursor, text[i], client); 485 return cursor; 486} 487 488TextPosition TextDocument::insert_at(const TextPosition& position, char ch, const Client* client) 489{ 490 bool automatic_indentation_enabled = client ? client->is_automatic_indentation_enabled() : false; 491 size_t m_soft_tab_width = client ? client->soft_tab_width() : 4; 492 493 bool at_head = position.column() == 0; 494 bool at_tail = position.column() == line(position.line()).length(); 495 if (ch == '\n') { 496 if (at_tail || at_head) { 497 String new_line_contents; 498 if (automatic_indentation_enabled && at_tail) { 499 size_t leading_spaces = 0; 500 auto& old_line = lines()[position.line()]; 501 for (size_t i = 0; i < old_line.length(); ++i) { 502 if (old_line.characters()[i] == ' ') 503 ++leading_spaces; 504 else 505 break; 506 } 507 if (leading_spaces) 508 new_line_contents = String::repeated(' ', leading_spaces); 509 } 510 511 size_t row = position.line(); 512 Vector<char> line_content; 513 for (size_t i = position.column(); i < line(row).length(); i++) 514 line_content.append(line(row).characters()[i]); 515 insert_line(position.line() + (at_tail ? 1 : 0), make<TextDocumentLine>(*this, new_line_contents)); 516 notify_did_change(); 517 return { position.line() + 1, line(position.line() + 1).length() }; 518 } 519 auto new_line = make<TextDocumentLine>(*this); 520 new_line->append(*this, line(position.line()).characters() + position.column(), line(position.line()).length() - position.column()); 521 522 Vector<char> line_content; 523 for (size_t i = 0; i < new_line->length(); i++) 524 line_content.append(new_line->characters()[i]); 525 line(position.line()).truncate(*this, position.column()); 526 insert_line(position.line() + 1, move(new_line)); 527 notify_did_change(); 528 return { position.line() + 1, 0 }; 529 } 530 if (ch == '\t') { 531 size_t next_soft_tab_stop = ((position.column() + m_soft_tab_width) / m_soft_tab_width) * m_soft_tab_width; 532 size_t spaces_to_insert = next_soft_tab_stop - position.column(); 533 for (size_t i = 0; i < spaces_to_insert; ++i) { 534 line(position.line()).insert(*this, position.column(), ' '); 535 } 536 notify_did_change(); 537 return { position.line(), next_soft_tab_stop }; 538 } 539 line(position.line()).insert(*this, position.column(), ch); 540 notify_did_change(); 541 return { position.line(), position.column() + 1 }; 542} 543 544void TextDocument::remove(const TextRange& unnormalized_range) 545{ 546 if (!unnormalized_range.is_valid()) 547 return; 548 549 auto range = unnormalized_range.normalized(); 550 551 // First delete all the lines in between the first and last one. 552 for (size_t i = range.start().line() + 1; i < range.end().line();) { 553 remove_line(i); 554 range.end().set_line(range.end().line() - 1); 555 } 556 557 if (range.start().line() == range.end().line()) { 558 // Delete within same line. 559 auto& line = this->line(range.start().line()); 560 bool whole_line_is_selected = range.start().column() == 0 && range.end().column() == line.length(); 561 562 if (whole_line_is_selected) { 563 line.clear(*this); 564 } else { 565 auto before_selection = String(line.characters(), line.length()).substring(0, range.start().column()); 566 auto after_selection = String(line.characters(), line.length()).substring(range.end().column(), line.length() - range.end().column()); 567 StringBuilder builder(before_selection.length() + after_selection.length()); 568 builder.append(before_selection); 569 builder.append(after_selection); 570 line.set_text(*this, builder.to_string()); 571 } 572 } else { 573 // Delete across a newline, merging lines. 574 ASSERT(range.start().line() == range.end().line() - 1); 575 auto& first_line = line(range.start().line()); 576 auto& second_line = line(range.end().line()); 577 auto before_selection = String(first_line.characters(), first_line.length()).substring(0, range.start().column()); 578 auto after_selection = String(second_line.characters(), second_line.length()).substring(range.end().column(), second_line.length() - range.end().column()); 579 StringBuilder builder(before_selection.length() + after_selection.length()); 580 builder.append(before_selection); 581 builder.append(after_selection); 582 583 first_line.set_text(*this, builder.to_string()); 584 remove_line(range.end().line()); 585 } 586 587 if (lines().is_empty()) { 588 append_line(make<TextDocumentLine>(*this)); 589 } 590 591 notify_did_change(); 592} 593 594TextRange TextDocument::range_for_entire_line(size_t line_index) const 595{ 596 if (line_index >= line_count()) 597 return {}; 598 return { { line_index, 0 }, { line_index, line(line_index).length() } }; 599} 600 601}