Serenity Operating System
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}