Serenity Operating System
at master 594 lines 19 kB view raw
1/* 2 * Copyright (c) 2020, the SerenityOS developers. 3 * 4 * SPDX-License-Identifier: BSD-2-Clause 5 */ 6 7#include <AK/CharacterTypes.h> 8#include <AK/ScopeGuard.h> 9#include <AK/ScopedValueRollback.h> 10#include <AK/StringBuilder.h> 11#include <AK/TemporaryChange.h> 12#include <LibCore/File.h> 13#include <LibLine/Editor.h> 14#include <stdio.h> 15#include <sys/wait.h> 16#include <unistd.h> 17 18namespace { 19constexpr u32 ctrl(char c) { return c & 0x3f; } 20} 21 22namespace Line { 23 24Function<bool(Editor&)> Editor::find_internal_function(StringView name) 25{ 26#define __ENUMERATE(internal_name) \ 27 if (name == #internal_name) \ 28 return EDITOR_INTERNAL_FUNCTION(internal_name); 29 30 ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(__ENUMERATE) 31 32 return {}; 33} 34 35void Editor::search_forwards() 36{ 37 ScopedValueRollback inline_search_cursor_rollback { m_inline_search_cursor }; 38 StringBuilder builder; 39 builder.append(Utf32View { m_buffer.data(), m_inline_search_cursor }); 40 auto search_phrase = builder.to_deprecated_string(); 41 if (m_search_offset_state == SearchOffsetState::Backwards) 42 --m_search_offset; 43 if (m_search_offset > 0) { 44 ScopedValueRollback search_offset_rollback { m_search_offset }; 45 --m_search_offset; 46 if (search(search_phrase, true)) { 47 m_search_offset_state = SearchOffsetState::Forwards; 48 search_offset_rollback.set_override_rollback_value(m_search_offset); 49 } else { 50 m_search_offset_state = SearchOffsetState::Unbiased; 51 } 52 } else { 53 m_search_offset_state = SearchOffsetState::Unbiased; 54 m_chars_touched_in_the_middle = buffer().size(); 55 m_cursor = 0; 56 m_buffer.clear(); 57 insert(search_phrase); 58 m_refresh_needed = true; 59 } 60} 61 62void Editor::search_backwards() 63{ 64 ScopedValueRollback inline_search_cursor_rollback { m_inline_search_cursor }; 65 StringBuilder builder; 66 builder.append(Utf32View { m_buffer.data(), m_inline_search_cursor }); 67 auto search_phrase = builder.to_deprecated_string(); 68 if (m_search_offset_state == SearchOffsetState::Forwards) 69 ++m_search_offset; 70 if (search(search_phrase, true)) { 71 m_search_offset_state = SearchOffsetState::Backwards; 72 ++m_search_offset; 73 } else { 74 m_search_offset_state = SearchOffsetState::Unbiased; 75 --m_search_offset; 76 } 77} 78 79void Editor::cursor_left_word() 80{ 81 if (m_cursor > 0) { 82 auto skipped_at_least_one_character = false; 83 for (;;) { 84 if (m_cursor == 0) 85 break; 86 if (skipped_at_least_one_character && !is_ascii_alphanumeric(m_buffer[m_cursor - 1])) // stop *after* a non-alnum, but only if it changes the position 87 break; 88 skipped_at_least_one_character = true; 89 --m_cursor; 90 } 91 } 92 m_inline_search_cursor = m_cursor; 93} 94 95void Editor::cursor_left_character() 96{ 97 if (m_cursor > 0) 98 --m_cursor; 99 m_inline_search_cursor = m_cursor; 100} 101 102void Editor::cursor_right_word() 103{ 104 if (m_cursor < m_buffer.size()) { 105 // Temporarily put a space at the end of our buffer, 106 // doing this greatly simplifies the logic below. 107 m_buffer.append(' '); 108 for (;;) { 109 if (m_cursor >= m_buffer.size()) 110 break; 111 if (!is_ascii_alphanumeric(m_buffer[++m_cursor])) 112 break; 113 } 114 m_buffer.take_last(); 115 } 116 m_inline_search_cursor = m_cursor; 117 m_search_offset = 0; 118} 119 120void Editor::cursor_right_character() 121{ 122 if (m_cursor < m_buffer.size()) { 123 ++m_cursor; 124 } 125 m_inline_search_cursor = m_cursor; 126 m_search_offset = 0; 127} 128 129void Editor::erase_character_backwards() 130{ 131 if (m_is_searching) { 132 return; 133 } 134 if (m_cursor == 0) { 135 fputc('\a', stderr); 136 fflush(stderr); 137 return; 138 } 139 remove_at_index(m_cursor - 1); 140 --m_cursor; 141 m_inline_search_cursor = m_cursor; 142 // We will have to redraw :( 143 m_refresh_needed = true; 144} 145 146void Editor::erase_character_forwards() 147{ 148 if (m_cursor == m_buffer.size()) { 149 fputc('\a', stderr); 150 fflush(stderr); 151 return; 152 } 153 remove_at_index(m_cursor); 154 m_refresh_needed = true; 155} 156 157void Editor::finish_edit() 158{ 159 fprintf(stderr, "<EOF>\n"); 160 if (!m_always_refresh) { 161 m_input_error = Error::Eof; 162 finish(); 163 really_quit_event_loop().release_value_but_fixme_should_propagate_errors(); 164 } 165} 166 167void Editor::kill_line() 168{ 169 for (size_t i = 0; i < m_cursor; ++i) 170 remove_at_index(0); 171 m_cursor = 0; 172 m_inline_search_cursor = m_cursor; 173 m_refresh_needed = true; 174} 175 176void Editor::erase_word_backwards() 177{ 178 // A word here is space-separated. `foo=bar baz` is two words. 179 bool has_seen_nonspace = false; 180 while (m_cursor > 0) { 181 if (is_ascii_space(m_buffer[m_cursor - 1])) { 182 if (has_seen_nonspace) 183 break; 184 } else { 185 has_seen_nonspace = true; 186 } 187 erase_character_backwards(); 188 } 189} 190 191void Editor::erase_to_end() 192{ 193 while (m_cursor < m_buffer.size()) 194 erase_character_forwards(); 195} 196 197void Editor::erase_to_beginning() 198{ 199} 200 201void Editor::transpose_characters() 202{ 203 if (m_cursor > 0 && m_buffer.size() >= 2) { 204 if (m_cursor < m_buffer.size()) 205 ++m_cursor; 206 swap(m_buffer[m_cursor - 1], m_buffer[m_cursor - 2]); 207 // FIXME: Update anchored styles too. 208 m_refresh_needed = true; 209 m_chars_touched_in_the_middle += 2; 210 } 211} 212 213void Editor::enter_search() 214{ 215 if (m_is_searching) { 216 // How did we get here? 217 VERIFY_NOT_REACHED(); 218 } else { 219 m_is_searching = true; 220 m_search_offset = 0; 221 m_pre_search_buffer.clear(); 222 for (auto code_point : m_buffer) 223 m_pre_search_buffer.append(code_point); 224 m_pre_search_cursor = m_cursor; 225 226 ensure_free_lines_from_origin(1 + num_lines()); 227 228 // Disable our own notifier so as to avoid interfering with the search editor. 229 m_notifier->set_enabled(false); 230 231 m_search_editor = Editor::construct(Configuration { Configuration::Eager, Configuration::NoSignalHandlers }); // Has anyone seen 'Inception'? 232 m_search_editor->initialize(); 233 add_child(*m_search_editor); 234 235 m_search_editor->on_display_refresh = [this](Editor& search_editor) { 236 // Remove the search editor prompt before updating ourselves (this avoids artifacts when we move the search editor around). 237 search_editor.cleanup().release_value_but_fixme_should_propagate_errors(); 238 239 StringBuilder builder; 240 builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() }); 241 if (!search(builder.to_deprecated_string(), false, false)) { 242 m_chars_touched_in_the_middle = m_buffer.size(); 243 m_refresh_needed = true; 244 m_buffer.clear(); 245 m_cursor = 0; 246 } 247 248 refresh_display().release_value_but_fixme_should_propagate_errors(); 249 250 // Move the search prompt below ours and tell it to redraw itself. 251 auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns); 252 search_editor.set_origin(prompt_end_line + m_origin_row, 1); 253 search_editor.m_refresh_needed = true; 254 }; 255 256 // Whenever the search editor gets a ^R, cycle between history entries. 257 m_search_editor->register_key_input_callback(ctrl('R'), [this](Editor& search_editor) { 258 ++m_search_offset; 259 search_editor.m_refresh_needed = true; 260 return false; // Do not process this key event 261 }); 262 263 // ^C should cancel the search. 264 m_search_editor->register_key_input_callback(ctrl('C'), [this](Editor& search_editor) { 265 search_editor.finish(); 266 m_reset_buffer_on_search_end = true; 267 search_editor.end_search(); 268 search_editor.deferred_invoke([&search_editor] { search_editor.really_quit_event_loop().release_value_but_fixme_should_propagate_errors(); }); 269 return false; 270 }); 271 272 // Whenever the search editor gets a backspace, cycle back between history entries 273 // unless we're at the zeroth entry, in which case, allow the deletion. 274 m_search_editor->register_key_input_callback(m_termios.c_cc[VERASE], [this](Editor& search_editor) { 275 if (m_search_offset > 0) { 276 --m_search_offset; 277 search_editor.m_refresh_needed = true; 278 return false; // Do not process this key event 279 } 280 281 search_editor.erase_character_backwards(); 282 return false; 283 }); 284 285 // ^L - This is a source of issues, as the search editor refreshes first, 286 // and we end up with the wrong order of prompts, so we will first refresh 287 // ourselves, then refresh the search editor, and then tell him not to process 288 // this event. 289 m_search_editor->register_key_input_callback(ctrl('L'), [this](auto& search_editor) { 290 fprintf(stderr, "\033[3J\033[H\033[2J"); // Clear screen. 291 292 // refresh our own prompt 293 { 294 TemporaryChange refresh_change { m_always_refresh, true }; 295 set_origin(1, 1); 296 m_refresh_needed = true; 297 refresh_display().release_value_but_fixme_should_propagate_errors(); 298 } 299 300 // move the search prompt below ours 301 // and tell it to redraw itself 302 auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns); 303 search_editor.set_origin(prompt_end_line + 1, 1); 304 search_editor.m_refresh_needed = true; 305 306 return false; 307 }); 308 309 // quit without clearing the current buffer 310 m_search_editor->register_key_input_callback('\t', [this](Editor& search_editor) { 311 search_editor.finish(); 312 m_reset_buffer_on_search_end = false; 313 return false; 314 }); 315 316 auto search_prompt = "\x1b[32msearch:\x1b[0m "sv; 317 318 // While the search editor is active, we do not want editing events. 319 m_is_editing = false; 320 321 auto search_string_result = m_search_editor->get_line(search_prompt); 322 323 // Grab where the search origin last was, anything up to this point will be cleared. 324 auto search_end_row = m_search_editor->m_origin_row; 325 326 remove_child(*m_search_editor); 327 m_search_editor = nullptr; 328 m_is_searching = false; 329 m_is_editing = true; 330 m_search_offset = 0; 331 332 // Re-enable the notifier after discarding the search editor. 333 m_notifier->set_enabled(true); 334 335 if (search_string_result.is_error()) { 336 // Somethine broke, fail 337 m_input_error = search_string_result.error(); 338 finish(); 339 return; 340 } 341 342 auto& search_string = search_string_result.value(); 343 344 // Manually cleanup the search line. 345 auto stderr_stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors(); 346 reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors(); 347 auto search_metrics = actual_rendered_string_metrics(search_string, {}); 348 auto metrics = actual_rendered_string_metrics(search_prompt, {}); 349 VT::clear_lines(0, metrics.lines_with_addition(search_metrics, m_num_columns) + search_end_row - m_origin_row - 1, *stderr_stream).release_value_but_fixme_should_propagate_errors(); 350 351 reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors(); 352 353 m_refresh_needed = true; 354 m_cached_prompt_valid = false; 355 m_chars_touched_in_the_middle = 1; 356 357 if (!m_reset_buffer_on_search_end || search_metrics.total_length == 0) { 358 // If the entry was empty, or we purposely quit without a newline, 359 // do not return anything; instead, just end the search. 360 end_search(); 361 return; 362 } 363 364 // Return the string, 365 finish(); 366 } 367} 368 369void Editor::transpose_words() 370{ 371 // A word here is contiguous alnums. `foo=bar baz` is three words. 372 373 // 'abcd,.:efg...' should become 'efg...,.:abcd' if caret is after 374 // 'efg...'. If it's in 'efg', it should become 'efg,.:abcd...' 375 // with the caret after it, which then becomes 'abcd...,.:efg' 376 // when alt-t is pressed a second time. 377 378 // Move to end of word under (or after) caret. 379 size_t cursor = m_cursor; 380 while (cursor < m_buffer.size() && !is_ascii_alphanumeric(m_buffer[cursor])) 381 ++cursor; 382 while (cursor < m_buffer.size() && is_ascii_alphanumeric(m_buffer[cursor])) 383 ++cursor; 384 385 // Move left over second word and the space to its right. 386 size_t end = cursor; 387 size_t start = cursor; 388 while (start > 0 && !is_ascii_alphanumeric(m_buffer[start - 1])) 389 --start; 390 while (start > 0 && is_ascii_alphanumeric(m_buffer[start - 1])) 391 --start; 392 size_t start_second_word = start; 393 394 // Move left over space between the two words. 395 while (start > 0 && !is_ascii_alphanumeric(m_buffer[start - 1])) 396 --start; 397 size_t start_gap = start; 398 399 // Move left over first word. 400 while (start > 0 && is_ascii_alphanumeric(m_buffer[start - 1])) 401 --start; 402 403 if (start != start_gap) { 404 // To swap the two words, swap each word (and the gap) individually, and then swap the whole range. 405 auto swap_range = [this](auto from, auto to) { 406 for (size_t i = 0; i < (to - from) / 2; ++i) 407 swap(m_buffer[from + i], m_buffer[to - 1 - i]); 408 }; 409 swap_range(start, start_gap); 410 swap_range(start_gap, start_second_word); 411 swap_range(start_second_word, end); 412 swap_range(start, end); 413 m_cursor = cursor; 414 // FIXME: Update anchored styles too. 415 m_refresh_needed = true; 416 m_chars_touched_in_the_middle += end - start; 417 } 418} 419 420void Editor::go_home() 421{ 422 m_cursor = 0; 423 m_inline_search_cursor = m_cursor; 424 m_search_offset = 0; 425} 426 427void Editor::go_end() 428{ 429 m_cursor = m_buffer.size(); 430 m_inline_search_cursor = m_cursor; 431 m_search_offset = 0; 432} 433 434void Editor::clear_screen() 435{ 436 warn("\033[3J\033[H\033[2J"); 437 auto stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors(); 438 VT::move_absolute(1, 1, *stream).release_value_but_fixme_should_propagate_errors(); 439 set_origin(1, 1); 440 m_refresh_needed = true; 441 m_cached_prompt_valid = false; 442} 443 444void Editor::insert_last_words() 445{ 446 if (!m_history.is_empty()) { 447 // FIXME: This isn't quite right: if the last arg was `"foo bar"` or `foo\ bar` (but not `foo\\ bar`), we should insert that whole arg as last token. 448 if (auto last_words = m_history.last().entry.split_view(' '); !last_words.is_empty()) 449 insert(last_words.last()); 450 } 451} 452 453void Editor::erase_alnum_word_backwards() 454{ 455 // A word here is contiguous alnums. `foo=bar baz` is three words. 456 bool has_seen_alnum = false; 457 while (m_cursor > 0) { 458 if (!is_ascii_alphanumeric(m_buffer[m_cursor - 1])) { 459 if (has_seen_alnum) 460 break; 461 } else { 462 has_seen_alnum = true; 463 } 464 erase_character_backwards(); 465 } 466} 467 468void Editor::erase_alnum_word_forwards() 469{ 470 // A word here is contiguous alnums. `foo=bar baz` is three words. 471 bool has_seen_alnum = false; 472 while (m_cursor < m_buffer.size()) { 473 if (!is_ascii_alphanumeric(m_buffer[m_cursor])) { 474 if (has_seen_alnum) 475 break; 476 } else { 477 has_seen_alnum = true; 478 } 479 erase_character_forwards(); 480 } 481} 482 483void Editor::case_change_word(Editor::CaseChangeOp change_op) 484{ 485 // A word here is contiguous alnums. `foo=bar baz` is three words. 486 while (m_cursor < m_buffer.size() && !is_ascii_alphanumeric(m_buffer[m_cursor])) 487 ++m_cursor; 488 size_t start = m_cursor; 489 while (m_cursor < m_buffer.size() && is_ascii_alphanumeric(m_buffer[m_cursor])) { 490 if (change_op == CaseChangeOp::Uppercase || (change_op == CaseChangeOp::Capital && m_cursor == start)) { 491 m_buffer[m_cursor] = to_ascii_uppercase(m_buffer[m_cursor]); 492 } else { 493 VERIFY(change_op == CaseChangeOp::Lowercase || (change_op == CaseChangeOp::Capital && m_cursor > start)); 494 m_buffer[m_cursor] = to_ascii_lowercase(m_buffer[m_cursor]); 495 } 496 ++m_cursor; 497 m_refresh_needed = true; 498 } 499} 500 501void Editor::capitalize_word() 502{ 503 case_change_word(CaseChangeOp::Capital); 504} 505 506void Editor::lowercase_word() 507{ 508 case_change_word(CaseChangeOp::Lowercase); 509} 510 511void Editor::uppercase_word() 512{ 513 case_change_word(CaseChangeOp::Uppercase); 514} 515 516void Editor::edit_in_external_editor() 517{ 518 auto const* editor_command = getenv("EDITOR"); 519 if (!editor_command) 520 editor_command = m_configuration.m_default_text_editor.characters(); 521 522 char file_path[] = "/tmp/line-XXXXXX"; 523 auto fd = mkstemp(file_path); 524 525 if (fd < 0) { 526 perror("mktemp"); 527 return; 528 } 529 530 { 531 auto write_fd = dup(fd); 532 auto stream = Core::File::adopt_fd(write_fd, Core::File::OpenMode::Write).release_value_but_fixme_should_propagate_errors(); 533 StringBuilder builder; 534 builder.append(Utf32View { m_buffer.data(), m_buffer.size() }); 535 auto bytes = builder.string_view().bytes(); 536 while (!bytes.is_empty()) { 537 auto nwritten = stream->write_some(bytes).release_value_but_fixme_should_propagate_errors(); 538 bytes = bytes.slice(nwritten); 539 } 540 lseek(fd, 0, SEEK_SET); 541 } 542 543 ScopeGuard remove_temp_file_guard { 544 [fd, file_path] { 545 close(fd); 546 unlink(file_path); 547 } 548 }; 549 550 Vector<char const*> args { editor_command, file_path, nullptr }; 551 auto pid = fork(); 552 553 if (pid == -1) { 554 perror("fork"); 555 return; 556 } 557 558 if (pid == 0) { 559 execvp(editor_command, const_cast<char* const*>(args.data())); 560 perror("execv"); 561 _exit(126); 562 } else { 563 int wstatus = 0; 564 do { 565 waitpid(pid, &wstatus, 0); 566 } while (errno == EINTR); 567 568 if (!(WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0)) 569 return; 570 } 571 572 { 573 auto file = Core::File::open({ file_path, strlen(file_path) }, Core::File::OpenMode::Read).release_value_but_fixme_should_propagate_errors(); 574 auto contents = file->read_until_eof().release_value_but_fixme_should_propagate_errors(); 575 StringView data { contents }; 576 while (data.ends_with('\n')) 577 data = data.substring_view(0, data.length() - 1); 578 579 m_cursor = 0; 580 m_chars_touched_in_the_middle = m_buffer.size(); 581 m_buffer.clear_with_capacity(); 582 m_refresh_needed = true; 583 584 Utf8View view { data }; 585 if (view.validate()) { 586 for (auto cp : view) 587 insert(cp); 588 } else { 589 for (auto ch : data) 590 insert(ch); 591 } 592 } 593} 594}