Coverage Report

Created: 2026-02-14 08:01

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/serenity/Userland/Libraries/LibLine/InternalFunctions.cpp
Line
Count
Source
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
18
namespace Line {
19
20
Function<bool(Editor&)> Editor::find_internal_function(StringView name)
21
0
{
22
0
#define __ENUMERATE(internal_name) \
23
0
    if (name == #internal_name)    \
24
0
        return EDITOR_INTERNAL_FUNCTION(internal_name);
25
26
0
    ENUMERATE_EDITOR_INTERNAL_FUNCTIONS(__ENUMERATE)
27
28
0
    return {};
29
0
}
30
31
void Editor::search_forwards()
32
0
{
33
0
    ScopedValueRollback inline_search_cursor_rollback { m_inline_search_cursor };
34
0
    StringBuilder builder;
35
0
    builder.append(Utf32View { m_buffer.data(), m_inline_search_cursor });
36
0
    auto search_phrase = builder.to_byte_string();
37
0
    if (m_search_offset_state == SearchOffsetState::Backwards)
38
0
        --m_search_offset;
39
0
    if (m_search_offset > 0) {
40
0
        ScopedValueRollback search_offset_rollback { m_search_offset };
41
0
        --m_search_offset;
42
0
        if (search(search_phrase, true)) {
43
0
            m_search_offset_state = SearchOffsetState::Forwards;
44
0
            search_offset_rollback.set_override_rollback_value(m_search_offset);
45
0
        } else {
46
0
            m_search_offset_state = SearchOffsetState::Unbiased;
47
0
        }
48
0
    } else {
49
0
        m_search_offset_state = SearchOffsetState::Unbiased;
50
0
        m_chars_touched_in_the_middle = buffer().size();
51
0
        m_cursor = 0;
52
0
        m_buffer.clear();
53
0
        insert(search_phrase);
54
0
        m_refresh_needed = true;
55
0
    }
56
0
}
57
58
void Editor::search_backwards()
59
0
{
60
0
    ScopedValueRollback inline_search_cursor_rollback { m_inline_search_cursor };
61
0
    StringBuilder builder;
62
0
    builder.append(Utf32View { m_buffer.data(), m_inline_search_cursor });
63
0
    auto search_phrase = builder.to_byte_string();
64
0
    if (m_search_offset_state == SearchOffsetState::Forwards)
65
0
        ++m_search_offset;
66
0
    if (search(search_phrase, true)) {
67
0
        m_search_offset_state = SearchOffsetState::Backwards;
68
0
        ++m_search_offset;
69
0
    } else {
70
0
        m_search_offset_state = SearchOffsetState::Unbiased;
71
0
        --m_search_offset;
72
0
    }
73
0
}
74
75
void Editor::cursor_left_word()
76
0
{
77
0
    auto has_seen_alnum = false;
78
0
    while (m_cursor) {
79
        // after seeing at least one alnum, stop just before a non-alnum
80
0
        if (not is_ascii_alphanumeric(m_buffer[m_cursor - 1])) {
81
0
            if (has_seen_alnum)
82
0
                break;
83
0
        } else {
84
0
            has_seen_alnum = true;
85
0
        }
86
87
0
        --m_cursor;
88
0
    }
89
0
    m_inline_search_cursor = m_cursor;
90
0
}
91
92
void Editor::cursor_left_nonspace_word()
93
0
{
94
0
    auto has_seen_nonspace = false;
95
0
    while (m_cursor) {
96
        // after seeing at least one non-space, stop just before a space
97
0
        if (is_ascii_space(m_buffer[m_cursor - 1])) {
98
0
            if (has_seen_nonspace)
99
0
                break;
100
0
        } else {
101
0
            has_seen_nonspace = true;
102
0
        }
103
104
0
        --m_cursor;
105
0
    }
106
0
    m_inline_search_cursor = m_cursor;
107
0
}
108
109
void Editor::cursor_left_character()
110
0
{
111
0
    if (m_cursor > 0) {
112
0
        size_t closest_cursor_left_offset;
113
0
        binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor - 1, &closest_cursor_left_offset);
114
0
        m_cursor = m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset];
115
0
    }
116
0
    m_inline_search_cursor = m_cursor;
117
0
}
118
119
void Editor::cursor_right_word()
120
0
{
121
0
    auto has_seen_alnum = false;
122
0
    while (m_cursor < m_buffer.size()) {
123
        // after seeing at least one alnum, stop at the first non-alnum
124
0
        if (not is_ascii_alphanumeric(m_buffer[m_cursor])) {
125
0
            if (has_seen_alnum)
126
0
                break;
127
0
        } else {
128
0
            has_seen_alnum = true;
129
0
        }
130
131
0
        ++m_cursor;
132
0
    }
133
0
    m_inline_search_cursor = m_cursor;
134
0
    m_search_offset = 0;
135
0
}
136
137
void Editor::cursor_right_nonspace_word()
138
0
{
139
0
    auto has_seen_nonspace = false;
140
0
    while (m_cursor < m_buffer.size()) {
141
        // after seeing at least one non-space, stop at the first space
142
0
        if (is_ascii_space(m_buffer[m_cursor])) {
143
0
            if (has_seen_nonspace)
144
0
                break;
145
0
        } else {
146
0
            has_seen_nonspace = true;
147
0
        }
148
149
0
        ++m_cursor;
150
0
    }
151
0
    m_inline_search_cursor = m_cursor;
152
0
    m_search_offset = 0;
153
0
}
154
155
void Editor::cursor_right_character()
156
0
{
157
0
    if (m_cursor < m_buffer.size()) {
158
0
        size_t closest_cursor_left_offset;
159
0
        binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor, &closest_cursor_left_offset);
160
0
        m_cursor = closest_cursor_left_offset + 1 >= m_cached_buffer_metrics.grapheme_breaks.size()
161
0
            ? m_buffer.size()
162
0
            : m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset + 1];
163
0
    }
164
0
    m_inline_search_cursor = m_cursor;
165
0
    m_search_offset = 0;
166
0
}
167
168
void Editor::erase_character_backwards()
169
0
{
170
0
    if (m_is_searching) {
171
0
        return;
172
0
    }
173
0
    if (m_cursor == 0) {
174
0
        fputc('\a', stderr);
175
0
        fflush(stderr);
176
0
        return;
177
0
    }
178
179
0
    size_t closest_cursor_left_offset;
180
0
    binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor - 1, &closest_cursor_left_offset);
181
0
    auto start_of_previous_grapheme = m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset];
182
0
    for (; m_cursor > start_of_previous_grapheme; --m_cursor)
183
0
        remove_at_index(m_cursor - 1);
184
185
0
    m_inline_search_cursor = m_cursor;
186
    // We will have to redraw :(
187
0
    m_refresh_needed = true;
188
0
}
189
190
void Editor::erase_character_forwards()
191
0
{
192
0
    if (m_cursor == m_buffer.size()) {
193
0
        fputc('\a', stderr);
194
0
        fflush(stderr);
195
0
        return;
196
0
    }
197
198
0
    size_t closest_cursor_left_offset;
199
0
    binary_search(m_cached_buffer_metrics.grapheme_breaks, m_cursor, &closest_cursor_left_offset);
200
0
    auto end_of_next_grapheme = closest_cursor_left_offset + 1 >= m_cached_buffer_metrics.grapheme_breaks.size()
201
0
        ? m_buffer.size()
202
0
        : m_cached_buffer_metrics.grapheme_breaks[closest_cursor_left_offset + 1];
203
0
    for (auto cursor = m_cursor; cursor < end_of_next_grapheme; ++cursor)
204
0
        remove_at_index(m_cursor);
205
0
    m_refresh_needed = true;
206
0
}
207
208
void Editor::finish_edit()
209
0
{
210
0
    fprintf(stderr, "<EOF>\n");
211
0
    if (!m_always_refresh) {
212
0
        m_input_error = Error::Eof;
213
0
        finish();
214
0
        really_quit_event_loop().release_value_but_fixme_should_propagate_errors();
215
0
    }
216
0
}
217
218
void Editor::kill_line()
219
0
{
220
0
    if (m_cursor == 0)
221
0
        return;
222
223
0
    m_last_erased.clear_with_capacity();
224
225
0
    for (size_t i = 0; i < m_cursor; ++i) {
226
0
        m_last_erased.append(m_buffer[0]);
227
0
        remove_at_index(0);
228
0
    }
229
0
    m_cursor = 0;
230
0
    m_inline_search_cursor = m_cursor;
231
0
    m_refresh_needed = true;
232
0
}
233
234
void Editor::erase_word_backwards()
235
0
{
236
0
    if (m_cursor == 0)
237
0
        return;
238
239
0
    m_last_erased.clear_with_capacity();
240
241
    // A word here is space-separated. `foo=bar baz` is two words.
242
0
    bool has_seen_nonspace = false;
243
0
    while (m_cursor > 0) {
244
0
        if (is_ascii_space(m_buffer[m_cursor - 1])) {
245
0
            if (has_seen_nonspace)
246
0
                break;
247
0
        } else {
248
0
            has_seen_nonspace = true;
249
0
        }
250
251
0
        m_last_erased.append(m_buffer[m_cursor - 1]);
252
0
        erase_character_backwards();
253
0
    }
254
255
0
    m_last_erased.reverse();
256
0
}
257
258
void Editor::erase_to_end()
259
0
{
260
0
    if (m_cursor == m_buffer.size())
261
0
        return;
262
263
0
    m_last_erased.clear_with_capacity();
264
265
0
    while (m_cursor < m_buffer.size()) {
266
0
        m_last_erased.append(m_buffer[m_cursor]);
267
0
        erase_character_forwards();
268
0
    }
269
0
}
270
271
void Editor::erase_to_beginning()
272
0
{
273
0
}
274
275
void Editor::insert_last_erased()
276
0
{
277
0
    insert(Utf32View { m_last_erased.data(), m_last_erased.size() });
278
0
}
279
280
void Editor::transpose_characters()
281
0
{
282
0
    if (m_cursor > 0 && m_buffer.size() >= 2) {
283
0
        if (m_cursor < m_buffer.size())
284
0
            ++m_cursor;
285
0
        swap(m_buffer[m_cursor - 1], m_buffer[m_cursor - 2]);
286
        // FIXME: Update anchored styles too.
287
0
        m_refresh_needed = true;
288
0
        m_chars_touched_in_the_middle += 2;
289
0
    }
290
0
}
291
292
void Editor::enter_search()
293
0
{
294
0
    if (m_is_searching) {
295
        // How did we get here?
296
0
        VERIFY_NOT_REACHED();
297
0
    } else {
298
0
        m_is_searching = true;
299
0
        m_search_offset = 0;
300
0
        m_pre_search_buffer.clear();
301
0
        for (auto code_point : m_buffer)
302
0
            m_pre_search_buffer.append(code_point);
303
0
        m_pre_search_cursor = m_cursor;
304
305
0
        ensure_free_lines_from_origin(1 + num_lines());
306
307
        // Disable our own notifier so as to avoid interfering with the search editor.
308
0
        m_notifier->set_enabled(false);
309
310
0
        m_search_editor = Editor::construct(Configuration { Configuration::Eager, Configuration::NoSignalHandlers }); // Has anyone seen 'Inception'?
311
0
        m_search_editor->initialize();
312
0
        add_child(*m_search_editor);
313
314
0
        m_search_editor->on_display_refresh = [this](Editor& search_editor) {
315
            // Remove the search editor prompt before updating ourselves (this avoids artifacts when we move the search editor around).
316
0
            search_editor.cleanup().release_value_but_fixme_should_propagate_errors();
317
318
0
            StringBuilder builder;
319
0
            builder.append(Utf32View { search_editor.buffer().data(), search_editor.buffer().size() });
320
0
            if (!search(builder.to_byte_string(), false, false)) {
321
0
                m_chars_touched_in_the_middle = m_buffer.size();
322
0
                m_refresh_needed = true;
323
0
                m_buffer.clear();
324
0
                m_cursor = 0;
325
0
            }
326
327
0
            refresh_display().release_value_but_fixme_should_propagate_errors();
328
329
            // Move the search prompt below ours and tell it to redraw itself.
330
0
            auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
331
0
            search_editor.set_origin(prompt_end_line + m_origin_row, 1);
332
0
            search_editor.m_refresh_needed = true;
333
0
        };
334
335
        // Whenever the search editor gets a ^R, cycle between history entries.
336
0
        m_search_editor->register_key_input_callback(ctrl('R'), [this](Editor& search_editor) {
337
0
            ++m_search_offset;
338
0
            search_editor.m_refresh_needed = true;
339
0
            return false; // Do not process this key event
340
0
        });
341
342
        // ^C should cancel the search.
343
0
        m_search_editor->register_key_input_callback(ctrl('C'), [this](Editor& search_editor) {
344
0
            search_editor.finish();
345
0
            m_reset_buffer_on_search_end = true;
346
0
            search_editor.end_search();
347
0
            search_editor.deferred_invoke([&search_editor] { search_editor.really_quit_event_loop().release_value_but_fixme_should_propagate_errors(); });
348
0
            return false;
349
0
        });
350
351
        // Whenever the search editor gets a backspace, cycle back between history entries
352
        // unless we're at the zeroth entry, in which case, allow the deletion.
353
0
        m_search_editor->register_key_input_callback(m_termios.c_cc[VERASE], [this](Editor& search_editor) {
354
0
            if (m_search_offset > 0) {
355
0
                --m_search_offset;
356
0
                search_editor.m_refresh_needed = true;
357
0
                return false; // Do not process this key event
358
0
            }
359
360
0
            search_editor.erase_character_backwards();
361
0
            return false;
362
0
        });
363
364
        // ^L - This is a source of issues, as the search editor refreshes first,
365
        // and we end up with the wrong order of prompts, so we will first refresh
366
        // ourselves, then refresh the search editor, and then tell it not to process
367
        // this event.
368
0
        m_search_editor->register_key_input_callback(ctrl('L'), [this](auto& search_editor) {
369
0
            fprintf(stderr, "\033[3J\033[H\033[2J"); // Clear screen.
370
371
            // refresh our own prompt
372
0
            {
373
0
                TemporaryChange refresh_change { m_always_refresh, true };
374
0
                set_origin(1, 1);
375
0
                m_refresh_needed = true;
376
0
                refresh_display().release_value_but_fixme_should_propagate_errors();
377
0
            }
378
379
            // move the search prompt below ours
380
            // and tell it to redraw itself
381
0
            auto prompt_end_line = current_prompt_metrics().lines_with_addition(m_cached_buffer_metrics, m_num_columns);
382
0
            search_editor.set_origin(prompt_end_line + 1, 1);
383
0
            search_editor.m_refresh_needed = true;
384
385
0
            return false;
386
0
        });
387
388
        // quit without clearing the current buffer
389
0
        m_search_editor->register_key_input_callback('\t', [this](Editor& search_editor) {
390
0
            search_editor.finish();
391
0
            m_reset_buffer_on_search_end = false;
392
0
            return false;
393
0
        });
394
395
0
        auto search_prompt = "\x1b[32msearch:\x1b[0m "sv;
396
397
        // While the search editor is active, we do not want editing events.
398
0
        m_is_editing = false;
399
400
0
        auto search_string_result = m_search_editor->get_line(search_prompt);
401
402
        // Grab where the search origin last was, anything up to this point will be cleared.
403
0
        auto search_end_row = m_search_editor->m_origin_row;
404
405
0
        remove_child(*m_search_editor);
406
0
        m_search_editor = nullptr;
407
0
        m_is_searching = false;
408
0
        m_is_editing = true;
409
0
        m_search_offset = 0;
410
411
        // Re-enable the notifier after discarding the search editor.
412
0
        m_notifier->set_enabled(true);
413
414
0
        if (search_string_result.is_error()) {
415
            // Somethine broke, fail
416
0
            m_input_error = search_string_result.error();
417
0
            finish();
418
0
            return;
419
0
        }
420
421
0
        auto& search_string = search_string_result.value();
422
423
        // Manually cleanup the search line.
424
0
        auto stderr_stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors();
425
0
        reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors();
426
0
        auto search_metrics = actual_rendered_string_metrics(search_string, {});
427
0
        auto metrics = actual_rendered_string_metrics(search_prompt, {});
428
0
        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();
429
430
0
        reposition_cursor(*stderr_stream).release_value_but_fixme_should_propagate_errors();
431
432
0
        m_refresh_needed = true;
433
0
        m_cached_prompt_valid = false;
434
0
        m_chars_touched_in_the_middle = 1;
435
436
0
        if (!m_reset_buffer_on_search_end || search_metrics.total_length == 0) {
437
            // If the entry was empty, or we purposely quit without a newline,
438
            // do not return anything; instead, just end the search.
439
0
            end_search();
440
0
            return;
441
0
        }
442
443
        // Return the string,
444
0
        finish();
445
0
    }
446
0
}
447
448
namespace {
449
Optional<u32> read_unicode_char()
450
0
{
451
    // FIXME: It would be ideal to somehow communicate that the line editor is
452
    // not operating in a normal mode and expects a character during the unicode
453
    // read (cursor mode? change current cell? change prompt? Something else?)
454
0
    StringBuilder builder;
455
456
0
    for (int i = 0; i < 4; ++i) {
457
0
        char c = 0;
458
0
        auto nread = read(0, &c, 1);
459
460
0
        if (nread <= 0)
461
0
            return {};
462
463
0
        builder.append(c);
464
465
0
        Utf8View search_char_utf8_view { builder.string_view() };
466
467
0
        if (search_char_utf8_view.validate())
468
0
            return *search_char_utf8_view.begin();
469
0
    }
470
471
0
    return {};
472
0
}
473
}
474
475
void Editor::search_character_forwards()
476
0
{
477
0
    auto optional_search_char = read_unicode_char();
478
0
    if (not optional_search_char.has_value())
479
0
        return;
480
0
    u32 search_char = optional_search_char.value();
481
482
0
    for (auto index = m_cursor + 1; index < m_buffer.size(); ++index) {
483
0
        if (m_buffer[index] == search_char) {
484
0
            m_cursor = index;
485
0
            return;
486
0
        }
487
0
    }
488
489
0
    fputc('\a', stderr);
490
0
    fflush(stderr);
491
0
}
492
493
void Editor::search_character_backwards()
494
0
{
495
0
    auto optional_search_char = read_unicode_char();
496
0
    if (not optional_search_char.has_value())
497
0
        return;
498
0
    u32 search_char = optional_search_char.value();
499
500
0
    for (auto index = m_cursor; index > 0; --index) {
501
0
        if (m_buffer[index - 1] == search_char) {
502
0
            m_cursor = index - 1;
503
0
            return;
504
0
        }
505
0
    }
506
507
0
    fputc('\a', stderr);
508
0
    fflush(stderr);
509
0
}
510
511
void Editor::transpose_words()
512
0
{
513
    // A word here is contiguous alnums. `foo=bar baz` is three words.
514
515
    // 'abcd,.:efg...' should become 'efg...,.:abcd' if caret is after
516
    // 'efg...'. If it's in 'efg', it should become 'efg,.:abcd...'
517
    // with the caret after it, which then becomes 'abcd...,.:efg'
518
    // when alt-t is pressed a second time.
519
520
    // Move to end of word under (or after) caret.
521
0
    size_t cursor = m_cursor;
522
0
    while (cursor < m_buffer.size() && !is_ascii_alphanumeric(m_buffer[cursor]))
523
0
        ++cursor;
524
0
    while (cursor < m_buffer.size() && is_ascii_alphanumeric(m_buffer[cursor]))
525
0
        ++cursor;
526
527
    // Move left over second word and the space to its right.
528
0
    size_t end = cursor;
529
0
    size_t start = cursor;
530
0
    while (start > 0 && !is_ascii_alphanumeric(m_buffer[start - 1]))
531
0
        --start;
532
0
    while (start > 0 && is_ascii_alphanumeric(m_buffer[start - 1]))
533
0
        --start;
534
0
    size_t start_second_word = start;
535
536
    // Move left over space between the two words.
537
0
    while (start > 0 && !is_ascii_alphanumeric(m_buffer[start - 1]))
538
0
        --start;
539
0
    size_t start_gap = start;
540
541
    // Move left over first word.
542
0
    while (start > 0 && is_ascii_alphanumeric(m_buffer[start - 1]))
543
0
        --start;
544
545
0
    if (start != start_gap) {
546
        // To swap the two words, swap each word (and the gap) individually, and then swap the whole range.
547
0
        auto swap_range = [this](auto from, auto to) {
548
0
            for (size_t i = 0; i < (to - from) / 2; ++i)
549
0
                swap(m_buffer[from + i], m_buffer[to - 1 - i]);
550
0
        };
551
0
        swap_range(start, start_gap);
552
0
        swap_range(start_gap, start_second_word);
553
0
        swap_range(start_second_word, end);
554
0
        swap_range(start, end);
555
0
        m_cursor = cursor;
556
        // FIXME: Update anchored styles too.
557
0
        m_refresh_needed = true;
558
0
        m_chars_touched_in_the_middle += end - start;
559
0
    }
560
0
}
561
562
void Editor::go_home()
563
0
{
564
0
    m_cursor = 0;
565
0
    m_inline_search_cursor = m_cursor;
566
0
    m_search_offset = 0;
567
0
}
568
569
void Editor::go_end()
570
0
{
571
0
    m_cursor = m_buffer.size();
572
0
    m_inline_search_cursor = m_cursor;
573
0
    m_search_offset = 0;
574
0
}
575
576
void Editor::clear_screen()
577
0
{
578
0
    warn("\033[3J\033[H\033[2J");
579
0
    auto stream = Core::File::standard_error().release_value_but_fixme_should_propagate_errors();
580
0
    VT::move_absolute(1, 1, *stream).release_value_but_fixme_should_propagate_errors();
581
0
    set_origin(1, 1);
582
0
    m_refresh_needed = true;
583
0
    m_cached_prompt_valid = false;
584
0
}
585
586
void Editor::insert_last_words()
587
0
{
588
0
    if (!m_history.is_empty()) {
589
        // 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.
590
0
        if (auto last_words = m_history.last().entry.split_view(' '); !last_words.is_empty())
591
0
            insert(last_words.last());
592
0
    }
593
0
}
594
595
void Editor::erase_alnum_word_backwards()
596
0
{
597
0
    if (m_cursor == 0)
598
0
        return;
599
600
0
    m_last_erased.clear_with_capacity();
601
602
    // A word here is contiguous alnums. `foo=bar baz` is three words.
603
0
    bool has_seen_alnum = false;
604
0
    while (m_cursor > 0) {
605
0
        if (!is_ascii_alphanumeric(m_buffer[m_cursor - 1])) {
606
0
            if (has_seen_alnum)
607
0
                break;
608
0
        } else {
609
0
            has_seen_alnum = true;
610
0
        }
611
612
0
        m_last_erased.append(m_buffer[m_cursor - 1]);
613
0
        erase_character_backwards();
614
0
    }
615
616
0
    m_last_erased.reverse();
617
0
}
618
619
void Editor::erase_alnum_word_forwards()
620
0
{
621
0
    if (m_cursor == m_buffer.size())
622
0
        return;
623
624
0
    m_last_erased.clear_with_capacity();
625
626
    // A word here is contiguous alnums. `foo=bar baz` is three words.
627
0
    bool has_seen_alnum = false;
628
0
    while (m_cursor < m_buffer.size()) {
629
0
        if (!is_ascii_alphanumeric(m_buffer[m_cursor])) {
630
0
            if (has_seen_alnum)
631
0
                break;
632
0
        } else {
633
0
            has_seen_alnum = true;
634
0
        }
635
636
0
        m_last_erased.append(m_buffer[m_cursor]);
637
0
        erase_character_forwards();
638
0
    }
639
0
}
640
641
void Editor::erase_spaces()
642
0
{
643
0
    while (m_cursor < m_buffer.size()) {
644
0
        if (is_ascii_space(m_buffer[m_cursor]))
645
0
            erase_character_forwards();
646
0
        else
647
0
            break;
648
0
    }
649
650
0
    while (m_cursor > 0) {
651
0
        if (is_ascii_space(m_buffer[m_cursor - 1]))
652
0
            erase_character_backwards();
653
0
        else
654
0
            break;
655
0
    }
656
0
}
657
658
void Editor::case_change_word(Editor::CaseChangeOp change_op)
659
0
{
660
    // A word here is contiguous alnums. `foo=bar baz` is three words.
661
0
    while (m_cursor < m_buffer.size() && !is_ascii_alphanumeric(m_buffer[m_cursor]))
662
0
        ++m_cursor;
663
0
    size_t start = m_cursor;
664
0
    while (m_cursor < m_buffer.size() && is_ascii_alphanumeric(m_buffer[m_cursor])) {
665
0
        if (change_op == CaseChangeOp::Uppercase || (change_op == CaseChangeOp::Capital && m_cursor == start)) {
666
0
            m_buffer[m_cursor] = to_ascii_uppercase(m_buffer[m_cursor]);
667
0
        } else {
668
0
            VERIFY(change_op == CaseChangeOp::Lowercase || (change_op == CaseChangeOp::Capital && m_cursor > start));
669
0
            m_buffer[m_cursor] = to_ascii_lowercase(m_buffer[m_cursor]);
670
0
        }
671
0
        ++m_cursor;
672
0
    }
673
674
0
    m_refresh_needed = true;
675
0
    m_chars_touched_in_the_middle = 1;
676
0
}
677
678
void Editor::capitalize_word()
679
0
{
680
0
    case_change_word(CaseChangeOp::Capital);
681
0
}
682
683
void Editor::lowercase_word()
684
0
{
685
0
    case_change_word(CaseChangeOp::Lowercase);
686
0
}
687
688
void Editor::uppercase_word()
689
0
{
690
0
    case_change_word(CaseChangeOp::Uppercase);
691
0
}
692
693
void Editor::edit_in_external_editor()
694
0
{
695
0
    auto const* editor_command = getenv("EDITOR");
696
0
    if (!editor_command)
697
0
        editor_command = m_configuration.m_default_text_editor.characters();
698
699
0
    char file_path[] = "/tmp/line-XXXXXX";
700
0
    auto fd = mkstemp(file_path);
701
702
0
    if (fd < 0) {
703
0
        perror("mktemp");
704
0
        return;
705
0
    }
706
707
0
    {
708
0
        auto write_fd = dup(fd);
709
0
        auto stream = Core::File::adopt_fd(write_fd, Core::File::OpenMode::Write).release_value_but_fixme_should_propagate_errors();
710
0
        StringBuilder builder;
711
0
        builder.append(Utf32View { m_buffer.data(), m_buffer.size() });
712
0
        auto bytes = builder.string_view().bytes();
713
0
        while (!bytes.is_empty()) {
714
0
            auto nwritten = stream->write_some(bytes).release_value_but_fixme_should_propagate_errors();
715
0
            bytes = bytes.slice(nwritten);
716
0
        }
717
0
        lseek(fd, 0, SEEK_SET);
718
0
    }
719
720
0
    ScopeGuard remove_temp_file_guard {
721
0
        [fd, file_path] {
722
0
            close(fd);
723
0
            unlink(file_path);
724
0
        }
725
0
    };
726
727
0
    Vector<char const*> args { editor_command, file_path, nullptr };
728
0
    auto pid = fork();
729
730
0
    if (pid == -1) {
731
0
        perror("fork");
732
0
        return;
733
0
    }
734
735
0
    if (pid == 0) {
736
0
        execvp(editor_command, const_cast<char* const*>(args.data()));
737
0
        perror("execv");
738
0
        _exit(126);
739
0
    } else {
740
0
        int wstatus = 0;
741
0
        do {
742
0
            waitpid(pid, &wstatus, 0);
743
0
        } while (errno == EINTR);
744
745
0
        if (!(WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0))
746
0
            return;
747
0
    }
748
749
0
    {
750
0
        auto file = Core::File::open({ file_path, strlen(file_path) }, Core::File::OpenMode::Read).release_value_but_fixme_should_propagate_errors();
751
0
        auto contents = file->read_until_eof().release_value_but_fixme_should_propagate_errors();
752
0
        StringView data { contents };
753
0
        while (data.ends_with('\n'))
754
0
            data = data.substring_view(0, data.length() - 1);
755
756
0
        m_cursor = 0;
757
0
        m_chars_touched_in_the_middle = m_buffer.size();
758
0
        m_buffer.clear_with_capacity();
759
0
        m_refresh_needed = true;
760
761
0
        Utf8View view { data };
762
0
        if (view.validate()) {
763
0
            for (auto cp : view)
764
0
                insert(cp);
765
0
        } else {
766
0
            for (auto ch : data)
767
0
                insert(ch);
768
0
        }
769
0
    }
770
0
}
771
}