• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 25348852825-3024

04 May 2026 11:18PM UTC coverage: 69.963% (+0.7%) from 69.226%
25348852825-3024

push

github

tstack
[ui] horizontal scroll should work on columns

Related to #1685

7 of 141 new or added lines in 5 files covered. (4.96%)

7760 existing lines in 84 files now uncovered.

57014 of 81492 relevant lines covered (69.96%)

622491.44 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

28.49
/src/textinput_curses.cc
1
/**
2
 * Copyright (c) 2025, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * 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
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <algorithm>
31

32
#include "textinput_curses.hh"
33

34
#include "base/attr_line.hh"
35
#include "base/auto_mem.hh"
36
#include "base/itertools.hh"
37
#include "base/keycodes.hh"
38
#include "base/string_attr_type.hh"
39
#include "config.h"
40
#include "data_parser.hh"
41
#include "data_scanner.hh"
42
#include "readline_highlighters.hh"
43
#include "sysclip.hh"
44
#include "unicase.h"
45
#include "unictype.h"
46
#include "ww898/cp_utf8.hpp"
47

48
using namespace std::chrono_literals;
49
using namespace lnav::roles::literals;
50

51
const attr_line_t&
52
textinput_curses::get_help_text()
38✔
53
{
54
    static const auto retval
55
        = attr_line_t()
36✔
56
              .append("Prompt Help"_h1)
36✔
57
              .append("\n\n")
36✔
58
              .append("Editing"_h2)
36✔
59
              .append("\n ")
36✔
60
              .append("\u2022"_list_glyph)
36✔
61
              .append(" ")
36✔
62
              .append("ESC"_hotkey)
36✔
63
              .append("       - Cancel editing\n ")
36✔
64
              .append("\u2022"_list_glyph)
36✔
65
              .append(" ")
36✔
66
              .append("CTRL-X"_hotkey)
36✔
67
              .append("    - Save and exit the editor\n ")
36✔
68
              .append("\u2022"_list_glyph)
36✔
69
              .append(" ")
36✔
70
              .append("HOME"_hotkey)
36✔
71
              .append("      - Move to the beginning of the buffer\n ")
36✔
72
              .append("\u2022"_list_glyph)
36✔
73
              .append(" ")
36✔
74
              .append("END"_hotkey)
36✔
75
              .append("       - Move to the end of the buffer\n ")
36✔
76
              .append("\u2022"_list_glyph)
36✔
77
              .append(" ")
36✔
78
              .append("CTRL-A"_hotkey)
36✔
79
              .append("    - Move to the beginning of the line\n ")
36✔
80
              .append("\u2022"_list_glyph)
36✔
81
              .append(" ")
36✔
82
              .append("CTRL-E"_hotkey)
36✔
83
              .append("    - Move to the end of the line\n ")
36✔
84
              .append("\u2022"_list_glyph)
36✔
85
              .append(" ")
36✔
86
              .append("CTRL-N"_hotkey)
36✔
87
              .append(
36✔
88
                  "    - Move down one line.  If a popup is open, move the "
89
                  "selection down.\n ")
90
              .append("\u2022"_list_glyph)
36✔
91
              .append(" ")
36✔
92
              .append("CTRL-P"_hotkey)
36✔
93
              .append(
36✔
94
                  "    - Move up one line.  If a popup is open, move the "
95
                  "selection up.\n ")
96
              .append("\u2022"_list_glyph)
36✔
97
              .append(" ")
36✔
98
              .append("ALT  \u2190"_hotkey)
36✔
99
              .append(" / ")
36✔
100
              .append("ALT-b"_hotkey)
36✔
101
              .append(" - Move to the previous word\n ")
36✔
102
              .append("\u2022"_list_glyph)
36✔
103
              .append(" ")
36✔
104
              .append("ALT  \u2192"_hotkey)
36✔
105
              .append(" / ")
36✔
106
              .append("ALT-f"_hotkey)
36✔
107
              .append(" - Move to the next word\n ")
36✔
108
              .append("\u2022"_list_glyph)
36✔
109
              .append(" ")
36✔
110
              .append("CTRL-T"_hotkey)
36✔
111
              .append("    - Transpose the two characters before the cursor\n ")
36✔
112
              .append("\u2022"_list_glyph)
36✔
113
              .append(" ")
36✔
114
              .append("ALT-l"_hotkey)
36✔
115
              .append(" / ")
36✔
116
              .append("ALT-u"_hotkey)
36✔
117
              .append(" - Lower/upper-case the next word\n ")
36✔
118
              .append("•"_list_glyph)
36✔
119
              .append(" ")
36✔
120
              .append("ALT-c"_hotkey)
36✔
121
              .append("    - Capitalize the next word\n ")
36✔
122
              .append("\u2022"_list_glyph)
36✔
123
              .append(" ")
36✔
124
              .append("CTRL-K"_hotkey)
36✔
125
              .append("    - Cut to the end of the line into the clipboard\n ")
36✔
126
              .append("\u2022"_list_glyph)
36✔
127
              .append(" ")
36✔
128
              .append("CTRL-U"_hotkey)
36✔
129
              .append(
36✔
130
                  "    - Cut from the beginning of the line to the cursor "
131
                  "into the clipboard\n ")
132
              .append("\u2022"_list_glyph)
36✔
133
              .append(" ")
36✔
134
              .append("CTRL-W"_hotkey)
36✔
135
              .append(" / ")
36✔
136
              .append("ALT-BS"_hotkey)
36✔
137
              .append(
36✔
138
                  " - Cut from the beginning of the previous word into "
139
                  "the clipboard\n ")
140
              .append("\u2022"_list_glyph)
36✔
141
              .append(" ")
36✔
142
              .append("ALT-d"_hotkey)
36✔
143
              .append(
36✔
144
                  "     - Cut to the end of the next word into the "
145
                  "clipboard\n ")
146
              .append("\u2022"_list_glyph)
36✔
147
              .append(" ")
36✔
148
              .append("Rt-click"_hotkey)
36✔
149
              .append("  - Copy selection to the system clipboard\n ")
36✔
150
              .append("\u2022"_list_glyph)
36✔
151
              .append(" ")
36✔
152
              .append("CTRL-Y"_hotkey)
36✔
153
              .append("    - Paste the clipboard content\n ")
36✔
154
              .append("\u2022"_list_glyph)
36✔
155
              .append(" ")
36✔
156
              .append("TAB/ENTER"_hotkey)
36✔
157
              .append(" - Accept a completion suggestion\n ")
36✔
158
              .append("\u2022"_list_glyph)
36✔
159
              .append(" ")
36✔
160
              .append("CTRL-_"_hotkey)
36✔
161
              .append("    - Undo a change\n ")
36✔
162
              .append("\u2022"_list_glyph)
36✔
163
              .append(" ")
36✔
164
              .append("CTRL-L"_hotkey)
36✔
165
              .append("    - Reformat the contents, if available\n ")
36✔
166
              .append("\u2022"_list_glyph)
36✔
167
              .append(" ")
36✔
168
              .append("CTRL-O"_hotkey)
36✔
169
              .append("    - Open the contents in an external editor\n")
36✔
170
              .append("\n")
36✔
171
              .append("Resizing"_h2)
36✔
172
              .append("\n ")
36✔
173
              .append("•"_list_glyph)
36✔
174
              .append(" ")
36✔
175
              .append("ALT-="_hotkey)
36✔
176
              .append("     - Grow the multi-line prompt by one line\n ")
36✔
177
              .append("•"_list_glyph)
36✔
178
              .append(" ")
36✔
179
              .append("ALT--"_hotkey)
36✔
180
              .append("     - Shrink the multi-line prompt by one line\n")
36✔
181
              .append("\n")
36✔
182
              .append("History"_h2)
36✔
183
              .append("\n ")
36✔
184
              .append("\u2022"_list_glyph)
36✔
185
              .append(" ")
36✔
186
              .append("\u2191"_hotkey)
36✔
187
              .append("      - Select content from the history\n ")
36✔
188
              .append("\u2022"_list_glyph)
36✔
189
              .append(" ")
36✔
190
              .append("CTRL-R"_hotkey)
36✔
191
              .append(" - Search history using current contents\n")
36✔
192
              .append("\n")
36✔
193
              .append("Searching"_h2)
36✔
194
              .append("\n ")
36✔
195
              .append("\u2022"_list_glyph)
36✔
196
              .append(" ")
36✔
197
              .append("CTRL-S"_hotkey)
36✔
198
              .append(" - Switch to search mode\n ")
36✔
199
              .append("\u2022"_list_glyph)
36✔
200
              .append(" ")
36✔
201
              .append("CTRL-R"_hotkey)
36✔
202
              .append(" - Search backwards for the string\n");
74✔
203

204
    return retval;
38✔
205
}
206

207
const std::vector<attr_line_t>&
UNCOV
208
textinput_curses::unhandled_input()
×
209
{
210
    static const auto retval = std::vector{
UNCOV
211
        attr_line_t()
×
212
            .append(" Notice: "_status_subtitle)
×
UNCOV
213
            .append(" Unhandled key press.  Press F1 for help")
×
UNCOV
214
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_ALERT_STATUS)),
×
215
    };
216

217
    return retval;
×
218
}
219

220
const std::vector<attr_line_t>&
UNCOV
221
textinput_curses::no_changes()
×
222
{
223
    static const auto retval = std::vector{
224
        attr_line_t()
×
225
            .append(" Notice: "_status_subtitle)
×
UNCOV
226
            .append(" No changes to undo")
×
UNCOV
227
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)),
×
228
    };
229

UNCOV
230
    return retval;
×
231
}
232

233
const std::vector<attr_line_t>&
UNCOV
234
textinput_curses::external_edit_failed()
×
235
{
236
    static const auto retval = std::vector{
UNCOV
237
        attr_line_t()
×
UNCOV
238
            .append(" Error: "_status_subtitle)
×
UNCOV
239
            .append(" Unable to write file for external edit")
×
UNCOV
240
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_ALERT_STATUS)),
×
241
    };
242

UNCOV
243
    return retval;
×
244
}
245

246
class textinput_mouse_delegate : public text_delegate {
247
public:
248
    textinput_mouse_delegate(textinput_curses* input) : tmd_input(input) {}
38✔
249

UNCOV
250
    bool text_handle_mouse(textview_curses& tc,
×
251
                           const listview_curses::display_line_content_t& dlc,
252
                           mouse_event& me) override
253
    {
UNCOV
254
        if (me.me_button == mouse_button_t::BUTTON_LEFT
×
UNCOV
255
            && me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED
×
UNCOV
256
            && dlc.is<listview_curses::main_content>())
×
257
        {
UNCOV
258
            ncinput ch{};
×
259

260
            ch.id = NCKEY_TAB;
×
261
            ch.eff_text[0] = '\t';
×
UNCOV
262
            ch.eff_text[1] = '\0';
×
UNCOV
263
            return this->tmd_input->handle_key(ch);
×
264
        }
265

UNCOV
266
        return false;
×
267
    }
268

269
    textinput_curses* tmd_input;
270
};
271

272
std::optional<line_range>
273
textinput_curses::selected_range::range_for_line(int y) const
3✔
274
{
275
    if (!this->contains_line(y)) {
3✔
UNCOV
276
        return std::nullopt;
×
277
    }
278

279
    line_range retval;
3✔
280

281
    if (y > this->sr_start.y) {
3✔
UNCOV
282
        retval.lr_start = 0;
×
283
    } else {
284
        retval.lr_start = this->sr_start.x;
3✔
285
    }
286
    if (y < this->sr_end.y) {
3✔
UNCOV
287
        retval.lr_end = -1;
×
288
    } else {
289
        retval.lr_end = this->sr_end.x;
3✔
290
    }
291
    retval.lr_unit = line_range::unit::codepoint;
3✔
292

293
    return retval;
3✔
294
}
295

296
textinput_curses::textinput_curses()
38✔
297
{
298
    this->vc_enabled = false;
38✔
299
    this->vc_children.emplace_back(&this->tc_popup);
38✔
300

301
    this->tc_popup.tc_cursor_role = role_t::VCR_CURSOR_LINE;
38✔
302
    this->tc_popup.tc_disabled_cursor_role = role_t::VCR_DISABLED_CURSOR_LINE;
38✔
303
    this->tc_popup.lv_border_left_role = role_t::VCR_POPUP_BORDER;
38✔
304
    this->tc_popup.set_visible(false);
38✔
305
    this->tc_popup.set_title("textinput popup");
76✔
306
    this->tc_popup.set_head_space(0_vl);
38✔
307
    this->tc_popup.set_selectable(true);
38✔
308
    this->tc_popup.set_show_scrollbar(true);
38✔
309
    this->tc_popup.set_default_role(role_t::VCR_POPUP);
38✔
310
    this->tc_popup.set_sub_source(&this->tc_popup_source);
38✔
311
    this->tc_popup.set_delegate(
38✔
312
        std::make_shared<textinput_mouse_delegate>(this));
76✔
313

314
    this->vc_children.emplace_back(&this->tc_help_view);
38✔
315
    this->tc_help_view.set_visible(false);
38✔
316
    this->tc_help_view.set_title("textinput help");
76✔
317
    this->tc_help_view.set_show_scrollbar(true);
38✔
318
    this->tc_help_view.set_default_role(role_t::VCR_STATUS);
38✔
319
    this->tc_help_view.set_sub_source(&this->tc_help_source);
38✔
320

321
    this->tc_on_help = [](textinput_curses& ti) {
38✔
322
        ti.tc_mode = mode_t::show_help;
×
323
        ti.set_needs_update();
×
324
    };
38✔
325
    this->tc_help_source.replace_with(get_help_text());
38✔
326

327
    this->set_content("");
38✔
328
}
38✔
329

330
void
331
textinput_curses::content_to_lines(std::string content, int x)
43✔
332
{
333
    auto al = attr_line_t(content);
43✔
334

335
    if (!this->tc_prefix.empty()) {
43✔
UNCOV
336
        al.insert(0, this->tc_prefix);
×
UNCOV
337
        x += this->tc_prefix.length();
×
338
    }
339
    highlight_syntax(this->tc_text_format, al, x);
43✔
340
    if (!this->tc_prefix.empty()) {
43✔
341
        // XXX yuck
UNCOV
342
        al.erase(0, this->tc_prefix.al_string.size());
×
343
    }
344
    this->tc_doc_meta = lnav::document::discover(al)
43✔
345
                            .with_text_format(this->tc_text_format)
43✔
346
                            .save_words()
43✔
347
                            .perform();
43✔
348
    this->tc_lines = al.split_lines();
43✔
349
    if (endswith(al.al_string, "\n")) {
43✔
UNCOV
350
        this->tc_lines.emplace_back();
×
351
    }
352
    if (this->tc_lines.empty()) {
43✔
353
        this->tc_lines.emplace_back();
40✔
354
    } else {
355
        this->apply_highlights();
3✔
356
    }
357
}
43✔
358

359
void
360
textinput_curses::set_content(std::string content)
40✔
361
{
362
    this->content_to_lines(std::move(content), this->tc_prefix.length());
40✔
363

364
    this->tc_change_log.clear();
40✔
365
    this->tc_marks.clear();
40✔
366
    this->tc_notice = std::nullopt;
40✔
367
    this->tc_left = 0;
40✔
368
    this->tc_top = 0;
40✔
369
    this->tc_cursor = {};
40✔
370
    this->tc_abort_requested = false;
40✔
371
    this->tc_drag_selection = std::nullopt;
40✔
372
    this->tc_selection = std::nullopt;
40✔
373
    this->clamp_point(this->tc_cursor);
40✔
374
    this->set_needs_update();
40✔
375
}
40✔
376

377
void
378
textinput_curses::set_height(int height)
1✔
379
{
380
    if (this->tc_height == height) {
1✔
381
        return;
1✔
382
    }
383

384
    this->tc_height = height;
×
UNCOV
385
    if (this->tc_height == 1) {
×
386
        if (this->tc_cursor.y != 0) {
×
UNCOV
387
            this->move_cursor_to(this->tc_cursor.copy_with_y(0));
×
388
        }
389
    }
390
    this->set_needs_update();
×
391
}
392

393
std::optional<view_curses*>
394
textinput_curses::contains(int x, int y)
×
395
{
UNCOV
396
    if (!this->vc_visible) {
×
397
        return std::nullopt;
×
398
    }
399

400
    auto child = view_curses::contains(x, y);
×
401
    if (child) {
×
402
        return child;
×
403
    }
404

405
    if (this->vc_x <= x && x < this->vc_x + this->vc_width && this->vc_y <= y
×
406
        && y < this->vc_y + this->tc_height)
×
407
    {
408
        return this;
×
409
    }
410
    return std::nullopt;
×
411
}
412

413
bool
414
textinput_curses::handle_mouse(mouse_event& me)
×
415
{
416
    ssize_t inner_height = this->tc_lines.size();
×
417

UNCOV
418
    log_debug("mouse here! button=%d state=%d x=%d y=%d",
×
419
              me.me_button,
420
              me.me_state,
421
              me.me_x,
422
              me.me_y);
423
    this->tc_notice = std::nullopt;
×
UNCOV
424
    this->tc_last_tick_after_input = std::nullopt;
×
425
    if (this->tc_mode == mode_t::show_help) {
×
426
        return this->tc_help_view.handle_mouse(me);
×
427
    }
UNCOV
428
    if (me.me_button == mouse_button_t::BUTTON_SCROLL_UP) {
×
UNCOV
429
        auto dim = this->get_visible_dimensions();
×
430
        if (this->tc_top > 0) {
×
431
            this->tc_top -= 1;
×
UNCOV
432
            if (this->tc_top + dim.dr_height - 2 < this->tc_cursor.y) {
×
433
                this->move_cursor_by({direction_t::up, 1});
×
434
            } else {
435
                this->ensure_cursor_visible();
×
436
            }
UNCOV
437
            this->set_needs_update();
×
438
        }
UNCOV
439
    } else if (me.me_button == mouse_button_t::BUTTON_SCROLL_DOWN) {
×
440
        auto dim = this->get_visible_dimensions();
×
441
        if (this->tc_top + dim.dr_height < inner_height) {
×
442
            this->tc_top += 1;
×
443
            if (this->tc_cursor.y <= this->tc_top) {
×
444
                this->move_cursor_by({direction_t::down, 1});
×
445
            } else {
446
                this->ensure_cursor_visible();
×
447
            }
448
            this->set_needs_update();
×
449
        }
450
    } else if (me.me_button == mouse_button_t::BUTTON_RIGHT) {
×
UNCOV
451
        this->copy_selection();
×
UNCOV
452
    } else if (me.me_button == mouse_button_t::BUTTON_LEFT) {
×
453
        this->tc_mode = mode_t::editing;
×
454
        auto adj_press_x = me.me_press_x;
×
455
        if (me.me_press_y == 0 && me.me_press_x > 0) {
×
UNCOV
456
            adj_press_x -= this->tc_prefix.column_width();
×
457
        }
458
        auto adj_x = me.me_x;
×
459
        if (me.me_y == 0 && me.me_x > 0) {
×
UNCOV
460
            adj_x -= this->tc_prefix.column_width();
×
461
        }
462
        auto inner_press_point = input_point{
463
            this->tc_left + adj_press_x,
×
464
            (int) this->tc_top + me.me_press_y,
×
465
        };
466
        this->clamp_point(inner_press_point);
×
467
        auto inner_point = input_point{
468
            this->tc_left + adj_x,
×
UNCOV
469
            (int) this->tc_top + me.me_y,
×
470
        };
UNCOV
471
        this->clamp_point(inner_point);
×
472

473
        this->tc_popup_type = popup_type_t::none;
×
474
        this->tc_popup.set_visible(false);
×
UNCOV
475
        this->tc_complete_range = std::nullopt;
×
476
        this->tc_cursor = inner_point;
×
UNCOV
477
        log_debug("new cursor x=%d y=%d", this->tc_cursor.x, this->tc_cursor.y);
×
478
        if (me.me_state == mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK) {
×
479
            const auto& al = this->tc_lines[this->tc_cursor.y];
×
480
            auto sf = string_fragment::from_str(al.al_string);
×
UNCOV
481
            auto cursor_sf = sf.sub_cell_range(this->tc_left + adj_x,
×
UNCOV
482
                                               this->tc_left + adj_x);
×
483
            auto ds = data_scanner(sf);
×
484

485
            while (true) {
UNCOV
486
                auto tok_res = ds.tokenize2(this->tc_text_format);
×
UNCOV
487
                if (!tok_res.has_value()) {
×
488
                    break;
×
489
                }
490

UNCOV
491
                auto tok = tok_res.value();
×
UNCOV
492
                log_debug("tok %d", tok.tr_token);
×
493

494
                auto tok_sf = (tok.tr_token == data_token_t::DT_QUOTED_STRING
×
UNCOV
495
                               && (cursor_sf.sf_begin
×
496
                                       == tok.to_string_fragment().sf_begin
×
497
                                   || cursor_sf.sf_begin
×
UNCOV
498
                                       == tok.to_string_fragment().sf_end - 1))
×
UNCOV
499
                    ? tok.to_string_fragment()
×
500
                    : tok.inner_string_fragment();
×
501
                log_debug("tok %d:%d  curs %d:%d",
×
502
                          tok_sf.sf_begin,
503
                          tok_sf.sf_end,
504
                          cursor_sf.sf_begin,
505
                          cursor_sf.sf_end);
506
                if (tok_sf.contains(cursor_sf)
×
507
                    && tok.tr_token != data_token_t::DT_WHITE)
×
508
                {
509
                    log_debug("hit!");
×
510
                    auto group_tok
511
                        = ds.find_matching_bracket(this->tc_text_format, tok);
×
512
                    if (group_tok) {
×
UNCOV
513
                        tok_sf = group_tok.value().to_string_fragment();
×
514
                    }
515
                    auto tok_start = input_point{
UNCOV
516
                        (int) sf.byte_to_column_index(tok_sf.sf_begin)
×
517
                            - this->tc_left,
×
UNCOV
518
                        this->tc_cursor.y,
×
519
                    };
520
                    auto tok_end = input_point{
UNCOV
521
                        (int) sf.byte_to_column_index(tok_sf.sf_end)
×
522
                            - this->tc_left,
×
UNCOV
523
                        this->tc_cursor.y,
×
524
                    };
525

526
                    log_debug("st %d:%d", tok_start.x, tok_end.x);
×
UNCOV
527
                    this->tc_drag_selection = std::nullopt;
×
528
                    this->tc_selection
529
                        = selected_range::from_mouse(tok_start, tok_end);
×
UNCOV
530
                    this->set_needs_update();
×
531
                }
532
            }
UNCOV
533
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_PRESSED) {
×
UNCOV
534
            this->tc_selection = std::nullopt;
×
UNCOV
535
            this->tc_cursor_anchor = inner_press_point;
×
UNCOV
536
            this->tc_drag_selection = selected_range::from_mouse(
×
UNCOV
537
                this->tc_cursor_anchor, inner_point);
×
UNCOV
538
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED) {
×
UNCOV
539
            this->tc_drag_selection = selected_range::from_mouse(
×
UNCOV
540
                this->tc_cursor_anchor, inner_point);
×
541
            this->set_needs_update();
×
542
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
×
UNCOV
543
            this->tc_drag_selection = std::nullopt;
×
544
            if (inner_press_point == inner_point) {
×
545
                this->tc_selection = std::nullopt;
×
546
            } else {
547
                this->tc_selection = selected_range::from_mouse(
×
548
                    this->tc_cursor_anchor, inner_point);
×
549
            }
UNCOV
550
            this->set_needs_update();
×
551
        }
552
        this->ensure_cursor_visible();
×
553
    }
554

UNCOV
555
    return true;
×
556
}
557

558
bool
UNCOV
559
textinput_curses::handle_help_key(const ncinput& ch)
×
560
{
561
    switch (ch.id) {
×
562
        case ' ':
×
563
        case 'b':
564
        case 'j':
565
        case 'k':
566
        case 'g':
567
        case 'G':
568
        case NCKEY_HOME:
569
        case NCKEY_END:
570
        case NCKEY_UP:
571
        case NCKEY_DOWN:
572
        case NCKEY_PGUP:
573
        case NCKEY_PGDOWN: {
UNCOV
574
            log_debug("passing key press to help view");
×
575
            return this->tc_help_view.handle_key(ch);
×
576
        }
577
        default: {
×
UNCOV
578
            log_debug("switching back to editing from help");
×
579
            this->tc_mode = mode_t::editing;
×
580
            this->tc_help_view.set_visible(false);
×
581
            if (this->tc_on_change) {
×
UNCOV
582
                this->tc_on_change(*this);
×
583
            }
UNCOV
584
            this->set_needs_update();
×
UNCOV
585
            return true;
×
586
        }
587
    }
588
}
589

590
bool
591
textinput_curses::handle_search_key(const ncinput& ch)
×
592
{
593
    if (ncinput_ctrl_p(&ch)) {
×
594
        switch (ch.id) {
×
595
            case 'a':
×
596
            case 'A':
597
            case 'e':
598
            case 'E': {
599
                this->tc_mode = mode_t::editing;
×
600
                return this->handle_key(ch);
×
601
            }
602
            case 's':
×
603
            case 'S': {
UNCOV
604
                if (!this->tc_search.empty()) {
×
605
                    this->tc_search_start_point = this->tc_cursor;
×
UNCOV
606
                    this->move_cursor_to_next_search_hit();
×
607
                }
UNCOV
608
                return true;
×
609
            }
610
            case 'r':
×
611
            case 'R': {
612
                if (!this->tc_search.empty()) {
×
UNCOV
613
                    this->tc_search_start_point = this->tc_cursor;
×
614
                    this->move_cursor_to_prev_search_hit();
×
615
                }
UNCOV
616
                return true;
×
617
            }
618
        }
619
        return false;
×
620
    }
621

622
    switch (ch.id) {
×
UNCOV
623
        case NCKEY_ESC:
×
624
            this->tc_mode = mode_t::editing;
×
625
            this->set_needs_update();
×
626
            return true;
×
627
        case NCKEY_BACKSPACE: {
×
UNCOV
628
            if (!this->tc_search.empty()) {
×
629
                if (this->tc_search_found.has_value()) {
×
630
                    this->tc_search.pop_back();
×
631
                    auto compile_res = lnav::pcre2pp::code::from(
632
                        lnav::pcre2pp::quote(this->tc_search), PCRE2_CASELESS);
×
633
                    this->tc_search_code = compile_res.unwrap().to_shared();
×
UNCOV
634
                } else {
×
635
                    this->tc_search.clear();
×
636
                    this->tc_search_code.reset();
×
637
                }
638
                this->move_cursor_to_next_search_hit();
×
639
            }
UNCOV
640
            return true;
×
641
        }
642
        case NCKEY_ENTER: {
×
UNCOV
643
            this->tc_search_start_point = this->tc_cursor;
×
644
            this->move_cursor_to_next_search_hit();
×
645
            return true;
×
646
        }
UNCOV
647
        case NCKEY_LEFT:
×
648
        case NCKEY_RIGHT:
649
        case NCKEY_UP:
650
        case NCKEY_DOWN: {
UNCOV
651
            this->tc_mode = mode_t::editing;
×
UNCOV
652
            this->handle_key(ch);
×
UNCOV
653
            return true;
×
654
        }
UNCOV
655
        default: {
×
656
            char utf8[32];
657
            size_t index = 0;
×
UNCOV
658
            for (const auto eff_ch : ch.eff_text) {
×
659
                if (eff_ch == 0) {
×
660
                    break;
×
661
                }
UNCOV
662
                ww898::utf::utf8::write(eff_ch,
×
663
                                        [&utf8, &index](const char bits) {
×
664
                                            utf8[index] = bits;
×
665
                                            index += 1;
×
UNCOV
666
                                        });
×
667
            }
668
            if (index > 0) {
×
669
                utf8[index] = 0;
×
670

UNCOV
671
                if (!this->tc_search_found.has_value()) {
×
672
                    this->tc_search.clear();
×
673
                }
674
                this->tc_search.append(utf8);
×
675
                if (!this->tc_search.empty()) {
×
676
                    auto compile_res = lnav::pcre2pp::code::from(
677
                        lnav::pcre2pp::quote(this->tc_search), PCRE2_CASELESS);
×
678
                    this->tc_search_code = compile_res.unwrap().to_shared();
×
679
                    this->move_cursor_to_next_search_hit();
×
680
                }
681
            }
UNCOV
682
            return true;
×
683
        }
684
    }
685

686
    return false;
687
}
688

689
void
UNCOV
690
textinput_curses::move_cursor_to_next_search_hit()
×
691
{
UNCOV
692
    if (this->tc_search_code == nullptr) {
×
693
        return;
×
694
    }
695

UNCOV
696
    auto x = this->tc_search_start_point.x;
×
697
    if (this->tc_search_found && !this->tc_search_found.value()) {
×
UNCOV
698
        this->tc_search_start_point.y = 0;
×
699
    }
700
    this->tc_search_found = false;
×
701
    for (auto y = this->tc_search_start_point.y;
×
UNCOV
702
         y < (ssize_t) this->tc_lines.size();
×
703
         y++)
704
    {
705
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
×
706

707
        const auto& al = this->tc_lines[y];
×
708
        auto byte_x = al.column_to_byte_index(x);
×
709
        auto after_x_sf = al.to_string_fragment().substr(byte_x);
×
710
        auto find_res = this->tc_search_code->capture_from(after_x_sf)
×
UNCOV
711
                            .into(md)
×
712
                            .matches()
×
713
                            .ignore_error();
×
714
        if (find_res) {
×
715
            this->tc_cursor.x
716
                = al.byte_to_column_index(find_res.value().f_all.sf_end);
×
UNCOV
717
            this->tc_cursor.y = y;
×
718
            log_debug(
×
719
                "search found %d:%d", this->tc_cursor.x, this->tc_cursor.y);
UNCOV
720
            this->tc_search_found = true;
×
721
            this->ensure_cursor_visible();
×
722
            break;
×
723
        }
724
        x = 0;
×
725
    }
UNCOV
726
    this->set_needs_update();
×
727
}
728

729
void
730
textinput_curses::move_cursor_to_prev_search_hit()
×
731
{
UNCOV
732
    auto max_x = std::make_optional(this->tc_search_start_point.x);
×
UNCOV
733
    if (this->tc_search_found && !this->tc_search_found.value()) {
×
734
        this->tc_search_start_point.y = this->tc_lines.size() - 1;
×
735
    }
736
    this->tc_search_found = false;
×
UNCOV
737
    for (auto y = this->tc_search_start_point.y; y >= 0; y--) {
×
738
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
×
739

UNCOV
740
        const auto& al = this->tc_lines[y];
×
UNCOV
741
        auto before_x_sf = al.to_string_fragment();
×
742
        if (max_x) {
×
743
            before_x_sf = before_x_sf.sub_cell_range(0, max_x.value());
×
744
        }
UNCOV
745
        auto find_res = this->tc_search_code->capture_from(before_x_sf)
×
746
                            .into(md)
×
747
                            .matches()
×
748
                            .ignore_error();
×
749
        if (find_res) {
×
750
            auto new_input_point = input_point{
UNCOV
751
                (int) al.byte_to_column_index(find_res.value().f_all.sf_end),
×
752
                y,
753
            };
754
            if (new_input_point != this->tc_cursor) {
×
755
                this->tc_cursor = new_input_point;
×
756
                this->tc_search_found = true;
×
UNCOV
757
                this->ensure_cursor_visible();
×
758
                break;
×
759
            }
760
        }
761
        max_x = std::nullopt;
×
762
    }
UNCOV
763
    this->set_needs_update();
×
764
}
765

766
void
767
textinput_curses::command_indent(indent_mode_t mode)
×
768
{
769
    log_debug("indenting line: %d", this->tc_cursor.y);
×
770

771
    if (this->tc_cursor.y == 0 && !this->tc_prefix.empty()) {
×
UNCOV
772
        return;
×
773
    }
774

UNCOV
775
    int indent_amount = 0;
×
UNCOV
776
    switch (mode) {
×
777
        case indent_mode_t::left:
×
778
        case indent_mode_t::clear_left:
779
            indent_amount = 0;
×
780
            break;
×
781
        case indent_mode_t::right:
×
782
            indent_amount = 4;
×
783
            break;
×
784
    }
785
    auto& al = this->tc_lines[this->tc_cursor.y];
×
UNCOV
786
    auto line_sf = al.to_string_fragment();
×
UNCOV
787
    const auto [before, after]
×
UNCOV
788
        = line_sf.split_when([](auto ch) { return !isspace(ch); });
×
789
    auto indent_iter = std::lower_bound(this->tc_doc_meta.m_indents.begin(),
×
790
                                        this->tc_doc_meta.m_indents.end(),
791
                                        before.length());
×
792
    if (indent_iter != this->tc_doc_meta.m_indents.end()) {
×
793
        if (mode == indent_mode_t::left || mode == indent_mode_t::clear_left) {
×
794
            if (indent_iter == this->tc_doc_meta.m_indents.begin()) {
×
795
                indent_amount = 0;
×
796
            } else {
UNCOV
797
                indent_amount = *std::prev(indent_iter);
×
798
            }
799
        } else if (before.empty()) {
×
800
            indent_amount = *indent_iter;
×
801
        } else {
802
            auto next_indent_iter = std::next(indent_iter);
×
UNCOV
803
            if (next_indent_iter == this->tc_doc_meta.m_indents.end()) {
×
UNCOV
804
                indent_amount += *indent_iter;
×
805
            } else {
806
                indent_amount = *next_indent_iter;
×
807
            }
808
        }
809
    }
810
    auto sel_len = (before.empty() && mode == indent_mode_t::clear_left)
×
UNCOV
811
        ? line_sf.column_width()
×
UNCOV
812
        : before.length();
×
813
    this->tc_selection = selected_range::from_key(
×
814
        this->tc_cursor.copy_with_x(0), this->tc_cursor.copy_with_x(sel_len));
×
815
    auto indent = std::string(indent_amount, ' ');
×
UNCOV
816
    auto old_cursor = this->tc_cursor;
×
UNCOV
817
    this->replace_selection(indent);
×
UNCOV
818
    this->tc_cursor.x = indent.length() - sel_len + old_cursor.x;
×
819
}
820

821
void
UNCOV
822
textinput_curses::command_down(const ncinput& ch)
×
823
{
824
    if (this->tc_popup.is_visible()) {
×
825
        this->tc_popup.handle_key(ch);
×
826
        if (this->tc_on_popup_change) {
×
827
            this->tc_in_popup_change = true;
×
828
            this->tc_on_popup_change(*this);
×
UNCOV
829
            this->tc_in_popup_change = false;
×
830
        }
831
    } else {
832
        ssize_t inner_height = this->tc_lines.size();
×
UNCOV
833
        if (ncinput_shift_p(&ch)) {
×
UNCOV
834
            if (!this->tc_selection) {
×
835
                this->tc_cursor_anchor = this->tc_cursor;
×
836
            }
837
        }
838
        if (this->tc_cursor.y + 1 < inner_height) {
×
UNCOV
839
            this->move_cursor_by({direction_t::down, 1});
×
840
        } else {
841
            this->move_cursor_to({
×
842
                (int) this->tc_lines[this->tc_cursor.y].column_width(),
×
UNCOV
843
                (int) this->tc_lines.size() - 1,
×
844
            });
845
        }
846
        if (ncinput_shift_p(&ch)) {
×
847
            this->tc_selection = selected_range::from_key(
×
848
                this->tc_cursor_anchor, this->tc_cursor);
×
849
        }
850
    }
851
}
852

853
void
UNCOV
854
textinput_curses::command_up(const ncinput& ch)
×
855
{
UNCOV
856
    if (this->tc_popup.is_visible()) {
×
UNCOV
857
        this->tc_popup.handle_key(ch);
×
UNCOV
858
        if (this->tc_on_popup_change) {
×
UNCOV
859
            this->tc_in_popup_change = true;
×
UNCOV
860
            this->tc_on_popup_change(*this);
×
UNCOV
861
            this->tc_in_popup_change = false;
×
862
        }
863
    } else if (this->tc_height == 1) {
×
864
        if (this->tc_on_history_list) {
×
UNCOV
865
            this->tc_on_history_list(*this);
×
866
        }
867
    } else {
868
        if (ncinput_shift_p(&ch)) {
×
869
            log_debug("up shift");
×
UNCOV
870
            if (!this->tc_selection) {
×
UNCOV
871
                this->tc_cursor_anchor = this->tc_cursor;
×
872
            }
873
        }
874
        if (this->tc_cursor.y > 0) {
×
875
            this->move_cursor_by({direction_t::up, 1});
×
876
        } else {
UNCOV
877
            this->move_cursor_to({0, 0});
×
878
        }
UNCOV
879
        if (ncinput_shift_p(&ch)) {
×
UNCOV
880
            this->tc_selection = selected_range::from_key(
×
881
                this->tc_cursor_anchor, this->tc_cursor);
×
882
        }
883
    }
884
}
885

886
void
UNCOV
887
textinput_curses::kill_word_backward()
×
888
{
UNCOV
889
    log_debug("cutting to beginning of previous word");
×
890
    auto al_sf = this->tc_lines[this->tc_cursor.y].to_string_fragment();
×
UNCOV
891
    auto prev_word_start_opt = al_sf.prev_word(this->tc_cursor.x);
×
UNCOV
892
    if (!prev_word_start_opt && this->tc_cursor.x > 0) {
×
UNCOV
893
        prev_word_start_opt = 0;
×
894
    }
UNCOV
895
    if (prev_word_start_opt) {
×
UNCOV
896
        if (this->tc_cut_location != this->tc_cursor) {
×
UNCOV
897
            log_debug("  cursor moved since last cut, clearing clipboard");
×
UNCOV
898
            this->tc_clipboard.clear();
×
899
        }
900
        auto prev_word = al_sf.sub_cell_range(prev_word_start_opt.value(),
×
901
                                              this->tc_cursor.x);
902
        this->tc_clipboard.emplace_front(prev_word.to_string());
×
UNCOV
903
        this->sync_to_sysclip();
×
904
        this->tc_selection = selected_range::from_key(
×
905
            this->tc_cursor.copy_with_x(prev_word_start_opt.value()),
×
906
            this->tc_cursor);
×
907
        this->replace_selection(string_fragment{});
×
UNCOV
908
        this->tc_cut_location = this->tc_cursor;
×
909
    }
910
}
911

912
void
913
textinput_curses::kill_word_forward()
×
914
{
915
    log_debug("cutting to end of next word");
×
UNCOV
916
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
917
    auto al_sf = al.to_string_fragment();
×
918
    auto next_word_end_opt = al_sf.next_word(this->tc_cursor.x);
×
919
    auto end_x = (int) next_word_end_opt.value_or(al.column_width());
×
UNCOV
920
    if (end_x <= this->tc_cursor.x) {
×
921
        return;
×
922
    }
923
    if (this->tc_cut_location != this->tc_cursor) {
×
924
        log_debug("  cursor moved since last cut, clearing clipboard");
×
925
        this->tc_clipboard.clear();
×
926
    }
927
    auto killed = al_sf.sub_cell_range(this->tc_cursor.x, end_x);
×
928
    this->tc_clipboard.emplace_back(killed.to_string());
×
UNCOV
929
    this->sync_to_sysclip();
×
UNCOV
930
    this->tc_selection = selected_range::from_key(
×
UNCOV
931
        this->tc_cursor, this->tc_cursor.copy_with_x(end_x));
×
UNCOV
932
    this->replace_selection(string_fragment{});
×
UNCOV
933
    this->tc_cut_location = this->tc_cursor;
×
934
}
935

936
void
937
textinput_curses::change_word_case(uint32_t (*xform)(uint32_t))
×
938
{
UNCOV
939
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
940
    auto al_sf = al.to_string_fragment();
×
941
    int range_start;
UNCOV
942
    if (al_sf.curr_word(this->tc_cursor.x)) {
×
943
        range_start = this->tc_cursor.x;
×
944
    } else {
945
        auto next_start_opt = al_sf.next_word(this->tc_cursor.x);
×
946
        if (!next_start_opt) {
×
UNCOV
947
            return;
×
948
        }
UNCOV
949
        range_start = next_start_opt.value();
×
950
    }
951

952
    auto next_word_end_opt = al_sf.next_word(range_start);
×
UNCOV
953
    auto end_x = (int) next_word_end_opt.value_or(al.column_width());
×
954
    if (end_x <= range_start) {
×
UNCOV
955
        return;
×
956
    }
957
    auto transformed
UNCOV
958
        = al_sf.sub_cell_range(range_start, end_x).transform_codepoints(xform);
×
959
    auto start_cursor = this->tc_cursor.copy_with_x(range_start);
×
UNCOV
960
    auto end_cursor = this->tc_cursor.copy_with_x(end_x);
×
961
    this->tc_selection = selected_range::from_key(start_cursor, end_cursor);
×
962
    this->replace_selection(transformed);
×
963
    this->move_cursor_to(end_cursor);
×
964
}
965

966
void
UNCOV
967
textinput_curses::capitalize_word()
×
968
{
969
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
970
    auto al_sf = al.to_string_fragment();
×
UNCOV
971
    auto curr_word_opt = al_sf.curr_word(this->tc_cursor.x);
×
972
    int word_start;
973
    if (curr_word_opt) {
×
974
        word_start = curr_word_opt.value();
×
975
    } else {
UNCOV
976
        auto next_start_opt = al_sf.next_word(this->tc_cursor.x);
×
UNCOV
977
        if (!next_start_opt) {
×
978
            return;
×
979
        }
980
        word_start = next_start_opt.value();
×
981
    }
982

UNCOV
983
    auto next_word_end_opt = al_sf.next_word(word_start);
×
984
    auto end_x = (int) next_word_end_opt.value_or(al.column_width());
×
985
    if (end_x <= word_start) {
×
986
        return;
×
987
    }
988
    bool saw_letter = false;
×
989
    auto transformed
990
        = al_sf.sub_cell_range(word_start, end_x)
×
991
              .transform_codepoints([&saw_letter](uint32_t cp) -> uint32_t {
×
992
                  if (!uc_is_general_category_withtable(cp,
×
993
                                                        UC_CATEGORY_MASK_L)) {
994
                      return cp;
×
995
                  }
UNCOV
996
                  auto new_cp = saw_letter ? uc_tolower(cp) : uc_toupper(cp);
×
997
                  saw_letter = true;
×
998
                  return new_cp;
×
999
              });
×
UNCOV
1000
    auto start_cursor = this->tc_cursor.copy_with_x(word_start);
×
1001
    auto end_cursor = this->tc_cursor.copy_with_x(end_x);
×
UNCOV
1002
    this->tc_selection = selected_range::from_key(start_cursor, end_cursor);
×
1003
    this->replace_selection(transformed);
×
1004
    this->move_cursor_to(end_cursor);
×
1005
}
1006

1007
void
1008
textinput_curses::transpose_chars()
×
1009
{
1010
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
1011
    auto col_count = (int) al.column_width();
×
UNCOV
1012
    if (col_count < 2 || this->tc_cursor.x == 0) {
×
1013
        return;
×
1014
    }
1015

1016
    int swap_left;
1017
    int swap_right;
1018
    bool advance_cursor;
1019
    if (this->tc_cursor.x >= col_count) {
×
UNCOV
1020
        swap_left = col_count - 2;
×
1021
        swap_right = col_count - 1;
×
1022
        advance_cursor = false;
×
1023
    } else {
1024
        swap_left = this->tc_cursor.x - 1;
×
UNCOV
1025
        swap_right = this->tc_cursor.x;
×
1026
        advance_cursor = true;
×
1027
    }
1028

1029
    auto al_sf = al.to_string_fragment();
×
1030
    auto left_char = al_sf.sub_cell_range(swap_left, swap_right).to_string();
×
1031
    auto right_char
1032
        = al_sf.sub_cell_range(swap_right, swap_right + 1).to_string();
×
1033
    this->tc_selection
1034
        = selected_range::from_key(this->tc_cursor.copy_with_x(swap_left),
×
UNCOV
1035
                                   this->tc_cursor.copy_with_x(swap_right + 1));
×
1036
    this->replace_selection(right_char + left_char);
×
1037
    if (advance_cursor) {
×
UNCOV
1038
        this->move_cursor_to(this->tc_cursor.copy_with_x(swap_right + 1));
×
1039
    }
1040
}
1041

1042
bool
1043
textinput_curses::handle_key(const ncinput& ch)
4✔
1044
{
1045
    static const auto PREFIX_RE = lnav::pcre2pp::code::from_const(
1046
        R"(^\s*((?:-|\*|1\.|>)(?:\s+\[( |x|X)\])?\s*))");
4✔
1047
    static const auto PREFIX_OR_WS_RE = lnav::pcre2pp::code::from_const(
1048
        R"(^\s*(>\s*|(?:-|\*|1\.)?(?:\s+\[( |x|X)\])?\s+))");
4✔
1049
    thread_local auto md = lnav::pcre2pp::match_data::unitialized();
4✔
1050

1051
    if (this->tc_abort_requested && ch.id != NCKEY_ESC) {
4✔
1052
        this->tc_abort_requested = false;
×
UNCOV
1053
        this->set_needs_update();
×
1054
    }
1055
    if (this->tc_notice) {
4✔
UNCOV
1056
        this->tc_notice = std::nullopt;
×
1057
        switch (ch.id) {
×
UNCOV
1058
            case NCKEY_F01:
×
1059
            case NCKEY_UP:
1060
            case NCKEY_DOWN:
1061
            case NCKEY_LEFT:
1062
            case NCKEY_RIGHT:
1063
                break;
×
1064
            default:
×
1065
                return true;
×
1066
        }
1067
    }
1068
    this->tc_last_tick_after_input = std::nullopt;
4✔
1069
    switch (this->tc_mode) {
4✔
1070
        case mode_t::searching:
×
UNCOV
1071
            return this->handle_search_key(ch);
×
1072
        case mode_t::show_help:
×
1073
            return this->handle_help_key(ch);
×
1074
        case mode_t::editing:
4✔
1075
            break;
4✔
1076
    }
1077

1078
    if (this->tc_mode == mode_t::searching) {
4✔
1079
        return this->handle_search_key(ch);
×
1080
    }
1081

1082
    auto dim = this->get_visible_dimensions();
4✔
1083
    auto inner_height = this->tc_lines.size();
4✔
1084
    auto bottom = inner_height - 1;
4✔
1085
    auto chid = ch.id;
4✔
1086

1087
    if (ch.id == NCKEY_PASTE) {
4✔
1088
        static const auto lf_re = lnav::pcre2pp::code::from_const("\r\n?");
1089
        auto paste_sf = string_fragment::from_c_str(ch.paste_content);
×
UNCOV
1090
        if (!this->tc_selection) {
×
1091
            this->tc_selection = selected_range::from_point(this->tc_cursor);
×
1092
        }
1093
        auto text = lf_re.replace(paste_sf, "\n");
×
UNCOV
1094
        log_debug("applying bracketed paste of size %zu", text.length());
×
1095
        this->replace_selection(text);
×
1096
        return true;
×
1097
    }
1098

1099
    if (ncinput_super_p(&ch)) {
4✔
1100
        switch (chid) {
×
1101
            case 'a': {
×
1102
                this->tc_selection
UNCOV
1103
                    = this->clamp_selection(selected_range::from_mouse(
×
UNCOV
1104
                        input_point::home(), input_point::end()));
×
1105
                this->set_needs_update();
×
UNCOV
1106
                break;
×
1107
            }
1108
            case 'c': {
×
1109
                this->copy_selection();
×
1110
                break;
×
1111
            }
UNCOV
1112
            case 'x': {
×
1113
                this->cut_selection();
×
1114
                break;
×
1115
            }
1116
            case 'z': {
×
UNCOV
1117
                this->undo_last_change();
×
1118
                break;
×
1119
            }
1120
        }
UNCOV
1121
        return true;
×
1122
    }
1123

1124
    if (ncinput_alt_p(&ch)) {
4✔
1125
        switch (chid) {
×
UNCOV
1126
            case 'b':
×
1127
            case 'B':
1128
            case NCKEY_LEFT: {
1129
                auto& al = this->tc_lines[this->tc_cursor.y];
×
UNCOV
1130
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
1131
                                        .prev_word(this->tc_cursor.x);
×
1132

1133
                this->move_cursor_to(
×
1134
                    this->tc_cursor.copy_with_x(next_col_opt.value_or(0)));
×
UNCOV
1135
                return true;
×
1136
            }
UNCOV
1137
            case 'f':
×
1138
            case 'F':
1139
            case NCKEY_RIGHT: {
1140
                auto& al = this->tc_lines[this->tc_cursor.y];
×
1141
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
1142
                                        .next_word(this->tc_cursor.x);
×
1143
                this->move_cursor_to(
×
1144
                    this->tc_cursor.copy_with_x(next_col_opt.value_or(
UNCOV
1145
                        this->tc_lines[this->tc_cursor.y].column_width())));
×
1146
                return true;
×
1147
            }
1148
            case 'c':
×
1149
            case 'C': {
1150
                this->capitalize_word();
×
1151
                return true;
×
1152
            }
1153
            case 'd':
×
1154
            case 'D': {
1155
                this->kill_word_forward();
×
1156
                return true;
×
1157
            }
1158
            case NCKEY_BACKSPACE: {
×
1159
                this->kill_word_backward();
×
1160
                return true;
×
1161
            }
1162
            case 'l':
×
1163
            case 'L': {
UNCOV
1164
                this->change_word_case(uc_tolower);
×
1165
                return true;
×
1166
            }
1167
            case 'u':
×
1168
            case 'U': {
1169
                this->change_word_case(uc_toupper);
×
1170
                return true;
×
1171
            }
1172
            case '=':
×
1173
            case '+': {
1174
                if (this->tc_on_height_change) {
×
1175
                    this->tc_on_height_change(*this, 1);
×
1176
                }
1177
                return true;
×
1178
            }
UNCOV
1179
            case '-':
×
1180
            case '_': {
UNCOV
1181
                if (this->tc_on_height_change) {
×
UNCOV
1182
                    this->tc_on_height_change(*this, -1);
×
1183
                }
1184
                return true;
×
1185
            }
1186
        }
1187
    }
1188

1189
    if (ncinput_ctrl_p(&ch)) {
4✔
1190
        if (ncinput_shift_p(&ch) && chid == '-') {
×
1191
            chid = '_';  // XXX
×
1192
        }
UNCOV
1193
        switch (chid) {
×
UNCOV
1194
            case 'a':
×
1195
            case 'A': {
UNCOV
1196
                this->move_cursor_to(this->tc_cursor.copy_with_x(0));
×
UNCOV
1197
                return true;
×
1198
            }
UNCOV
1199
            case 'b':
×
1200
            case 'B': {
1201
                chid = NCKEY_LEFT;
×
1202
                break;
×
1203
            }
1204
            case 'd':
×
1205
            case 'D': {
1206
                chid = NCKEY_DEL;
×
1207
                break;
×
1208
            }
1209
            case 'e':
×
1210
            case 'E': {
1211
                this->move_cursor_to(this->tc_cursor.copy_with_x(
×
1212
                    this->tc_lines[this->tc_cursor.y].column_width()));
×
UNCOV
1213
                return true;
×
1214
            }
1215
            case 'f':
×
1216
            case 'F': {
1217
                chid = NCKEY_RIGHT;
×
UNCOV
1218
                break;
×
1219
            }
UNCOV
1220
            case 'h':
×
1221
            case 'H': {
1222
                chid = NCKEY_BACKSPACE;
×
1223
                break;
×
1224
            }
1225
            case 'k':
×
1226
            case 'K': {
UNCOV
1227
                if (this->tc_selection) {
×
UNCOV
1228
                    this->cut_selection();
×
1229
                } else {
UNCOV
1230
                    log_debug("cutting from %d to end of line %d",
×
1231
                              this->tc_cursor.x,
1232
                              this->tc_cursor.y);
1233
                    if (this->tc_cursor != this->tc_cut_location) {
×
1234
                        log_debug("  cursor moved, clearing clipboard");
×
1235
                        this->tc_clipboard.clear();
×
1236
                    }
1237
                    auto& al = this->tc_lines[this->tc_cursor.y];
×
1238
                    auto byte_index
1239
                        = al.column_to_byte_index(this->tc_cursor.x);
×
1240
                    this->tc_clipboard.emplace_back(
×
1241
                        al.subline(byte_index).al_string);
×
1242
                    this->tc_selection = selected_range::from_key(
×
1243
                        this->tc_cursor,
1244
                        this->tc_cursor.copy_with_x(al.column_width()));
×
1245
                    if (this->tc_selection->empty()
×
UNCOV
1246
                        && this->tc_cursor.y + 1
×
1247
                            < (ssize_t) this->tc_lines.size())
×
1248
                    {
UNCOV
1249
                        this->tc_clipboard.back().push_back('\n');
×
1250
                        this->tc_selection = selected_range::from_key(
×
1251
                            this->tc_cursor,
1252
                            input_point{0, this->tc_cursor.y + 1});
×
1253
                    }
1254
                    this->replace_selection(string_fragment{});
×
1255
                    this->tc_cut_location = this->tc_cursor;
×
1256
                }
1257
                this->sync_to_sysclip();
×
1258
                this->tc_drag_selection = std::nullopt;
×
1259
                this->update_lines();
×
UNCOV
1260
                return true;
×
1261
            }
1262
            case 'l':
×
1263
            case 'L': {
1264
                log_debug("reformat content");
×
1265
                if (this->tc_on_reformat) {
×
1266
                    this->tc_on_reformat(*this);
×
1267
                }
UNCOV
1268
                return true;
×
1269
            }
UNCOV
1270
            case 'n':
×
1271
            case 'N': {
UNCOV
1272
                chid = NCKEY_DOWN;
×
UNCOV
1273
                break;
×
1274
            }
1275
            case 'o':
×
1276
            case 'O': {
UNCOV
1277
                log_debug("opening in external editor");
×
UNCOV
1278
                if (this->tc_on_external_open) {
×
UNCOV
1279
                    this->tc_on_external_open(*this);
×
1280
                }
1281
                return true;
×
1282
            }
UNCOV
1283
            case 'p':
×
1284
            case 'P': {
UNCOV
1285
                chid = NCKEY_UP;
×
1286
                break;
×
1287
            }
1288
            case 'r':
×
1289
            case 'R': {
1290
                if (this->tc_on_history_search) {
×
1291
                    this->tc_on_history_search(*this);
×
1292
                }
UNCOV
1293
                return true;
×
1294
            }
1295
            case 's':
×
1296
            case 'S': {
UNCOV
1297
                if (this->tc_height > 1) {
×
1298
                    log_debug("switching to search mode from edit");
×
1299
                    this->tc_mode = mode_t::searching;
×
1300
                    this->tc_search_start_point = this->tc_cursor;
×
1301
                    this->tc_search_found = std::nullopt;
×
1302
                    this->set_needs_update();
×
1303
                }
1304
                return true;
×
1305
            }
UNCOV
1306
            case 't':
×
1307
            case 'T': {
1308
                this->transpose_chars();
×
1309
                return true;
×
1310
            }
UNCOV
1311
            case 'u':
×
1312
            case 'U': {
1313
                log_debug("cutting to beginning of line");
×
1314
                auto& al = this->tc_lines[this->tc_cursor.y];
×
1315
                auto byte_index = al.column_to_byte_index(this->tc_cursor.x);
×
UNCOV
1316
                if (this->tc_cursor != this->tc_cut_location) {
×
1317
                    log_debug("  cursor moved, clearing clipboard");
×
1318
                    this->tc_clipboard.clear();
×
1319
                }
1320
                this->tc_clipboard.emplace_back(
×
UNCOV
1321
                    al.subline(0, byte_index).al_string);
×
UNCOV
1322
                this->sync_to_sysclip();
×
1323
                this->tc_selection = selected_range::from_key(
×
UNCOV
1324
                    this->tc_cursor.copy_with_x(0), this->tc_cursor);
×
UNCOV
1325
                this->replace_selection(string_fragment{});
×
UNCOV
1326
                this->tc_cut_location = this->tc_cursor;
×
1327
                this->tc_selection = std::nullopt;
×
UNCOV
1328
                this->tc_drag_selection = std::nullopt;
×
1329
                this->update_lines();
×
1330
                return true;
×
1331
            }
UNCOV
1332
            case 'w':
×
1333
            case 'W': {
1334
                this->kill_word_backward();
×
1335
                return true;
×
1336
            }
1337
            case 'x':
×
1338
            case 'X': {
1339
                log_debug("performing action");
×
UNCOV
1340
                this->blur();
×
1341
                if (this->tc_on_perform) {
×
UNCOV
1342
                    this->tc_on_perform(*this);
×
1343
                }
1344
                return true;
×
1345
            }
1346
            case 'y':
×
1347
            case 'Y': {
1348
                log_debug("pasting clipboard contents");
×
UNCOV
1349
                for (const auto& clipping : this->tc_clipboard) {
×
1350
                    auto& al = this->tc_lines[this->tc_cursor.y];
×
1351
                    al.insert(al.column_to_byte_index(this->tc_cursor.x),
×
1352
                              clipping);
1353
                    const auto clip_sf = string_fragment::from_str(clipping);
×
1354
                    const auto clip_cols
1355
                        = clip_sf
UNCOV
1356
                              .find_left_boundary(clip_sf.length(),
×
1357
                                                  string_fragment::tag1{'\n'})
×
1358
                              .column_width();
×
1359
                    auto line_count = clip_sf.count('\n');
×
1360
                    if (line_count > 0) {
×
1361
                        this->tc_cursor.x = 0;
×
1362
                    } else {
1363
                        this->tc_cursor.x += clip_cols;
×
1364
                    }
1365
                    this->tc_cursor.y += line_count;
×
1366
                    this->tc_selection = std::nullopt;
×
1367
                    this->tc_drag_selection = std::nullopt;
×
1368
                    this->update_lines();
×
1369
                }
1370
                return true;
×
1371
            }
UNCOV
1372
            case ']': {
×
1373
                if (this->tc_popup.is_visible()) {
×
1374
                    this->tc_popup_type = popup_type_t::none;
×
1375
                    this->tc_popup.set_visible(false);
×
1376
                    this->tc_complete_range = std::nullopt;
×
1377
                    this->set_needs_update();
×
1378
                } else {
1379
                    this->abort();
×
1380
                }
1381

1382
                this->tc_selection = std::nullopt;
×
UNCOV
1383
                this->tc_drag_selection = std::nullopt;
×
1384
                return true;
×
1385
            }
1386
            case '_': {
×
1387
                this->undo_last_change();
×
UNCOV
1388
                return true;
×
1389
            }
UNCOV
1390
            default: {
×
1391
                this->tc_notice = unhandled_input();
×
UNCOV
1392
                this->set_needs_update();
×
1393
                return false;
×
1394
            }
1395
        }
1396
    }
1397

1398
    switch (chid) {
4✔
1399
        case NCKEY_ESC:
×
1400
        case KEY_CTRL(']'): {
UNCOV
1401
            if (this->tc_popup.is_visible()) {
×
UNCOV
1402
                if (this->tc_on_popup_cancel) {
×
UNCOV
1403
                    this->tc_on_popup_cancel(*this);
×
1404
                }
UNCOV
1405
                this->tc_popup_type = popup_type_t::none;
×
1406
                this->tc_popup.set_visible(false);
×
1407
                this->tc_complete_range = std::nullopt;
×
UNCOV
1408
                this->set_needs_update();
×
UNCOV
1409
            } else if (chid != NCKEY_ESC || this->tc_abort_requested) {
×
1410
                this->abort();
×
1411
            } else {
UNCOV
1412
                this->tc_abort_requested = true;
×
1413
                this->set_needs_update();
×
1414
            }
1415

UNCOV
1416
            this->tc_selection = std::nullopt;
×
1417
            this->tc_drag_selection = std::nullopt;
×
1418
            return true;
×
1419
        }
1420
        case NCKEY_ENTER: {
1✔
1421
            if (this->tc_popup.is_visible()) {
1✔
1422
                this->tc_popup.set_visible(false);
×
1423
                if (this->tc_on_completion) {
×
1424
                    this->tc_on_completion(*this);
×
1425
                }
1426
                this->tc_popup_type = popup_type_t::none;
×
1427
                this->set_needs_update();
×
1428
            } else if (this->tc_height == 1) {
1✔
1429
                this->blur();
1✔
1430
                if (this->tc_on_perform) {
1✔
1431
                    this->tc_on_perform(*this);
1✔
1432
                }
1433
            } else {
1434
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
UNCOV
1435
                auto al_sf = al.to_string_fragment();
×
1436
                auto prefix_sf = al_sf.rtrim(" ");
×
UNCOV
1437
                auto indent = std::string("\n");
×
1438
                if (!this->tc_selection) {
×
1439
                    log_debug("checking for prefix");
×
1440
                    auto match_opt = PREFIX_OR_WS_RE.capture_from(al_sf)
×
1441
                                         .into(md)
×
UNCOV
1442
                                         .matches()
×
1443
                                         .ignore_error();
×
1444
                    if (match_opt) {
×
1445
                        log_debug("has prefix");
×
1446
                        this->tc_selection = selected_range::from_key(
×
1447
                            this->tc_cursor.copy_with_x(
1448
                                prefix_sf.column_width()),
×
1449
                            this->tc_cursor.copy_with_x(al_sf.column_width()));
×
1450
                        auto is_comment
1451
                            = al.al_attrs
×
UNCOV
1452
                            | lnav::itertools::find_if(
×
1453
                                  [](const string_attr& sa) {
×
UNCOV
1454
                                      return (sa.sa_type == &VC_ROLE)
×
1455
                                          && sa.sa_value.get<role_t>()
×
1456
                                          == role_t::VCR_COMMENT;
×
1457
                                  });
×
UNCOV
1458
                        if (!is_comment && !al.empty()
×
1459
                            && !md[1]->startswith(">")
×
UNCOV
1460
                            && match_opt->f_all.length() == al.length())
×
1461
                        {
1462
                            log_debug("clear left");
×
1463
                            this->command_indent(indent_mode_t::clear_left);
×
1464
                        } else if (this->is_cursor_at_end_of_line()) {
×
1465
                            indent += match_opt->f_all;
×
1466
                            if (md[2] && md[2]->front() != ' ') {
×
UNCOV
1467
                                indent[1 + md[2]->sf_begin] = ' ';
×
1468
                            }
1469
                        } else {
1470
                            indent.append(match_opt->f_all.length(), ' ');
×
1471
                            this->tc_selection
1472
                                = selected_range::from_point(this->tc_cursor);
×
1473
                        }
1474
                    } else {
1475
                        this->tc_selection
UNCOV
1476
                            = selected_range::from_point(this->tc_cursor);
×
UNCOV
1477
                        log_debug("no prefix, replace point: [%d:%d]",
×
1478
                                  this->tc_selection->sr_start.x,
1479
                                  this->tc_selection->sr_start.y);
1480
                    }
1481
                }
UNCOV
1482
                this->replace_selection(indent);
×
1483
            }
1484
            // TODO implement "double enter" to call tc_on_perform
1485
            return true;
1✔
1486
        }
UNCOV
1487
        case NCKEY_TAB: {
×
UNCOV
1488
            if (this->tc_popup.is_visible()) {
×
UNCOV
1489
                log_debug("performing completion");
×
UNCOV
1490
                this->tc_popup_type = popup_type_t::none;
×
UNCOV
1491
                this->tc_popup.set_visible(false);
×
UNCOV
1492
                if (this->tc_on_completion) {
×
UNCOV
1493
                    this->tc_on_completion(*this);
×
1494
                }
UNCOV
1495
                this->set_needs_update();
×
UNCOV
1496
            } else if (!this->tc_suggestion.empty()
×
UNCOV
1497
                       && this->is_cursor_at_end_of_line())
×
1498
            {
UNCOV
1499
                log_debug("inserting suggestion");
×
UNCOV
1500
                this->tc_selection = selected_range::from_key(this->tc_cursor,
×
UNCOV
1501
                                                              this->tc_cursor);
×
UNCOV
1502
                this->replace_selection(this->tc_suggestion);
×
UNCOV
1503
            } else if (this->tc_height == 1) {
×
UNCOV
1504
                log_debug("requesting completion at %d", this->tc_cursor.x);
×
UNCOV
1505
                if (this->tc_on_completion_request) {
×
UNCOV
1506
                    this->tc_on_completion_request(*this);
×
1507
                }
UNCOV
1508
            } else if (!this->tc_selection) {
×
UNCOV
1509
                if (!ncinput_shift_p(&ch)
×
1510
                    && (this->tc_cursor.x > 0
×
1511
                        && this->tc_lines[this->tc_cursor.y].al_string.back()
×
1512
                            != ' '))
1513
                {
UNCOV
1514
                    log_debug("requesting completion at %d", this->tc_cursor.x);
×
UNCOV
1515
                    if (this->tc_on_completion_request) {
×
UNCOV
1516
                        this->tc_on_completion_request(*this);
×
1517
                    }
1518
                    if (!this->tc_popup.is_visible()) {
×
UNCOV
1519
                        this->command_indent(indent_mode_t::right);
×
1520
                    }
UNCOV
1521
                    return true;
×
1522
                }
1523

UNCOV
1524
                this->command_indent(ncinput_shift_p(&ch)
×
1525
                                         ? indent_mode_t::left
1526
                                         : indent_mode_t::right);
1527
            }
UNCOV
1528
            return true;
×
1529
        }
UNCOV
1530
        case NCKEY_HOME: {
×
UNCOV
1531
            this->move_cursor_to(input_point::home());
×
UNCOV
1532
            return true;
×
1533
        }
UNCOV
1534
        case NCKEY_END: {
×
UNCOV
1535
            this->move_cursor_to(input_point::end());
×
1536
            return true;
×
1537
        }
UNCOV
1538
        case NCKEY_PGUP: {
×
1539
            if (this->tc_cursor.y > 0) {
×
UNCOV
1540
                this->move_cursor_by({direction_t::up, (size_t) dim.dr_height});
×
1541
            }
1542
            return true;
×
1543
        }
UNCOV
1544
        case NCKEY_PGDOWN: {
×
UNCOV
1545
            if (this->tc_cursor.y < (ssize_t) bottom) {
×
UNCOV
1546
                this->move_cursor_by(
×
UNCOV
1547
                    {direction_t::down, (size_t) dim.dr_height});
×
1548
            }
UNCOV
1549
            return true;
×
1550
        }
1551
        case NCKEY_DEL: {
×
1552
            this->tc_selection = selected_range::from_key(
×
1553
                this->tc_cursor,
UNCOV
1554
                this->tc_cursor + movement{direction_t::right, 1});
×
UNCOV
1555
            this->replace_selection(string_fragment{});
×
UNCOV
1556
            break;
×
1557
        }
UNCOV
1558
        case NCKEY_BACKSPACE: {
×
UNCOV
1559
            if (this->tc_lines.size() == 1 && this->tc_lines.front().empty()) {
×
1560
                this->abort();
×
UNCOV
1561
            } else if (!this->tc_selection) {
×
UNCOV
1562
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
UNCOV
1563
                auto line_sf = al.to_string_fragment();
×
UNCOV
1564
                const auto [before, after] = line_sf.split_n(
×
1565
                    line_sf.column_to_byte_index(this->tc_cursor.x));
×
UNCOV
1566
                auto match_opt = PREFIX_RE.capture_from(before)
×
UNCOV
1567
                                     .into(md)
×
UNCOV
1568
                                     .matches()
×
1569
                                     .ignore_error();
×
1570

1571
                if (match_opt && !match_opt->f_all.empty()
×
UNCOV
1572
                    && match_opt->f_all.sf_end == this->tc_cursor.x)
×
1573
                {
1574
                    auto is_comment = al.al_attrs
×
1575
                        | lnav::itertools::find_if([](const string_attr& sa) {
×
UNCOV
1576
                                          return (sa.sa_type == &VC_ROLE)
×
1577
                                              && sa.sa_value.get<role_t>()
×
UNCOV
1578
                                              == role_t::VCR_COMMENT;
×
UNCOV
1579
                                      });
×
UNCOV
1580
                    if (!is_comment && md[1]) {
×
1581
                        this->tc_selection = selected_range::from_key(
×
UNCOV
1582
                            this->tc_cursor.copy_with_x(md[1]->sf_begin),
×
UNCOV
1583
                            this->tc_cursor);
×
1584
                        auto indent = std::string(
1585
                            md[1]->startswith(">") ? 0 : md[1]->length(), ' ');
×
1586

UNCOV
1587
                        this->replace_selection(indent);
×
UNCOV
1588
                        return true;
×
1589
                    }
1590
                } else {
1591
                    auto indent_iter
UNCOV
1592
                        = std::lower_bound(this->tc_doc_meta.m_indents.begin(),
×
1593
                                           this->tc_doc_meta.m_indents.end(),
UNCOV
1594
                                           this->tc_cursor.x);
×
1595
                    if (indent_iter != this->tc_doc_meta.m_indents.end()) {
×
UNCOV
1596
                        if (indent_iter != this->tc_doc_meta.m_indents.begin())
×
1597
                        {
UNCOV
1598
                            auto prev_indent_iter = std::prev(indent_iter);
×
UNCOV
1599
                            this->tc_selection = selected_range::from_key(
×
UNCOV
1600
                                this->tc_cursor.copy_with_x(*prev_indent_iter),
×
UNCOV
1601
                                this->tc_cursor);
×
1602
                        }
1603
                    }
1604
                }
UNCOV
1605
                if (!this->tc_selection) {
×
1606
                    this->tc_selection
UNCOV
1607
                        = selected_range::from_point_and_movement(
×
UNCOV
1608
                            this->tc_cursor, movement{direction_t::left, 1});
×
1609
                }
1610
            }
UNCOV
1611
            this->replace_selection(string_fragment{});
×
UNCOV
1612
            return true;
×
1613
        }
UNCOV
1614
        case NCKEY_UP: {
×
1615
            this->command_up(ch);
×
UNCOV
1616
            return true;
×
1617
        }
1618
        case NCKEY_DOWN: {
×
UNCOV
1619
            this->command_down(ch);
×
1620
            return true;
×
1621
        }
UNCOV
1622
        case NCKEY_LEFT: {
×
UNCOV
1623
            if (ncinput_shift_p(&ch)) {
×
UNCOV
1624
                if (!this->tc_selection) {
×
UNCOV
1625
                    this->tc_cursor_anchor = this->tc_cursor;
×
1626
                }
UNCOV
1627
                this->move_cursor_by({direction_t::left, 1});
×
UNCOV
1628
                this->tc_selection = selected_range::from_key(
×
1629
                    this->tc_cursor_anchor, this->tc_cursor);
×
UNCOV
1630
            } else if (this->tc_selection) {
×
UNCOV
1631
                this->tc_cursor = this->tc_selection->sr_start;
×
UNCOV
1632
                this->tc_selection = std::nullopt;
×
UNCOV
1633
                this->set_needs_update();
×
1634
            } else {
UNCOV
1635
                this->move_cursor_by({direction_t::left, 1});
×
1636
            }
UNCOV
1637
            return true;
×
1638
        }
UNCOV
1639
        case NCKEY_RIGHT: {
×
UNCOV
1640
            if (ncinput_shift_p(&ch)) {
×
UNCOV
1641
                if (!this->tc_selection) {
×
UNCOV
1642
                    this->tc_cursor_anchor = this->tc_cursor;
×
1643
                }
UNCOV
1644
                this->move_cursor_by({direction_t::right, 1});
×
1645
                this->tc_selection = selected_range::from_key(
×
UNCOV
1646
                    this->tc_cursor_anchor, this->tc_cursor);
×
UNCOV
1647
            } else if (this->tc_selection) {
×
UNCOV
1648
                this->tc_cursor = this->tc_selection->sr_end;
×
UNCOV
1649
                this->tc_selection = std::nullopt;
×
UNCOV
1650
                this->set_needs_update();
×
1651
            } else {
UNCOV
1652
                this->move_cursor_by({direction_t::right, 1});
×
1653
            }
1654
            return true;
×
1655
        }
1656
        case NCKEY_F01: {
×
1657
            if (this->tc_on_help) {
×
1658
                this->tc_on_help(*this);
×
1659
            }
1660
            return true;
×
1661
        }
UNCOV
1662
        case ' ': {
×
UNCOV
1663
            if (!this->tc_selection) {
×
UNCOV
1664
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
UNCOV
1665
                const auto sf = al.to_string_fragment();
×
UNCOV
1666
                if (PREFIX_RE.capture_from(sf).into(md).found_p() && md[2]
×
UNCOV
1667
                    && this->tc_cursor.x == md[2]->sf_begin)
×
1668
                {
1669
                    this->tc_selection = selected_range::from_key(
×
1670
                        this->tc_cursor,
1671
                        this->tc_cursor.copy_with_x(this->tc_cursor.x + 1));
×
1672

UNCOV
1673
                    auto repl = (md[2]->front() == ' ') ? "X"_frag : " "_frag;
×
UNCOV
1674
                    this->replace_selection(repl);
×
1675
                    return true;
×
1676
                }
1677

1678
                this->tc_selection
1679
                    = selected_range::from_point(this->tc_cursor);
×
1680
            }
1681
            this->replace_selection(" "_frag);
×
UNCOV
1682
            return true;
×
1683
        }
1684
        default: {
3✔
1685
            if (NCKEY_F00 <= ch.id && ch.id <= NCKEY_F60) {
3✔
UNCOV
1686
                this->tc_notice = unhandled_input();
×
UNCOV
1687
                this->set_needs_update();
×
1688
            } else {
1689
                char utf8[32];
1690
                size_t index = 0;
3✔
1691
                for (const auto eff_ch : ch.eff_text) {
6✔
1692
                    log_debug(" eff %x", eff_ch);
6✔
1693
                    if (eff_ch == 0) {
6✔
1694
                        break;
3✔
1695
                    }
1696
                    ww898::utf::utf8::write(eff_ch,
3✔
1697
                                            [&utf8, &index](const char bits) {
3✔
1698
                                                utf8[index] = bits;
3✔
1699
                                                index += 1;
3✔
1700
                                            });
3✔
1701
                }
1702
                if (index > 0) {
3✔
1703
                    utf8[index] = 0;
3✔
1704

1705
                    if (!this->tc_selection) {
3✔
1706
                        this->tc_selection
1707
                            = selected_range::from_point(this->tc_cursor);
3✔
1708
                    }
1709
                    this->replace_selection(string_fragment::from_c_str(utf8));
3✔
1710
                } else {
UNCOV
1711
                    this->tc_notice = unhandled_input();
×
UNCOV
1712
                    this->set_needs_update();
×
1713
                }
1714
            }
1715
            return true;
3✔
1716
        }
1717
    }
1718

UNCOV
1719
    return false;
×
1720
}
1721

1722
void
1723
textinput_curses::ensure_cursor_visible()
19✔
1724
{
1725
    if (!this->vc_enabled) {
19✔
1726
        return;
16✔
1727
    }
1728

1729
    auto dim = this->get_visible_dimensions();
3✔
1730
    auto orig_top = this->tc_top;
3✔
1731
    auto orig_left = this->tc_left;
3✔
1732
    auto orig_cursor = this->tc_cursor;
3✔
1733
    auto orig_max_cursor_x = this->tc_max_cursor_x;
3✔
1734

1735
    this->clamp_point(this->tc_cursor);
3✔
1736
    if (this->tc_cursor.y < 0) {
3✔
1737
        this->tc_cursor.y = 0;
×
1738
    }
1739
    if (this->tc_cursor.y >= (ssize_t) this->tc_lines.size()) {
3✔
UNCOV
1740
        this->tc_cursor.y = this->tc_lines.size() - 1;
×
1741
    }
1742
    if (this->tc_cursor.x < 0) {
3✔
UNCOV
1743
        this->tc_cursor.x = 0;
×
1744
    }
1745
    if (this->tc_cursor.x
6✔
1746
        >= (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1747
    {
1748
        this->tc_cursor.x = this->tc_lines[this->tc_cursor.y].column_width();
3✔
1749
    }
1750

1751
    if (this->tc_cursor.x <= this->tc_left) {
3✔
UNCOV
1752
        this->tc_left = this->tc_cursor.x;
×
UNCOV
1753
        if (this->tc_left > 0) {
×
UNCOV
1754
            this->tc_left -= 1;
×
1755
        }
1756
    }
1757
    if (this->tc_cursor.x >= this->tc_left + (dim.dr_width - 2)) {
3✔
UNCOV
1758
        this->tc_left = (this->tc_cursor.x - dim.dr_width) + 2;
×
1759
    }
1760
    if (this->tc_top < 0) {
3✔
UNCOV
1761
        this->tc_top = 0;
×
1762
    }
1763
    if (this->tc_top >= this->tc_cursor.y) {
3✔
1764
        this->tc_top = this->tc_cursor.y;
3✔
1765
        if (this->tc_top > 0) {
3✔
UNCOV
1766
            this->tc_top -= 1;
×
1767
        }
1768
    }
1769
    if (this->tc_height > 1
3✔
UNCOV
1770
        && this->tc_cursor.y + 1 >= this->tc_top + dim.dr_height)
×
1771
    {
UNCOV
1772
        this->tc_top = (this->tc_cursor.y + 1 - dim.dr_height) + 1;
×
1773
    }
1774
    if (this->tc_top + dim.dr_height > (ssize_t) this->tc_lines.size()) {
3✔
UNCOV
1775
        if ((ssize_t) this->tc_lines.size() > dim.dr_height) {
×
UNCOV
1776
            this->tc_top = this->tc_lines.size() - dim.dr_height + 1;
×
1777
        } else {
UNCOV
1778
            this->tc_top = 0;
×
1779
        }
1780
    }
1781
    if (!this->tc_in_popup_change && this->tc_popup.is_visible()
3✔
UNCOV
1782
        && this->tc_complete_range
×
1783
        && !this->tc_complete_range->contains(this->tc_cursor))
6✔
1784
    {
UNCOV
1785
        this->tc_popup.set_visible(false);
×
UNCOV
1786
        this->tc_complete_range = std::nullopt;
×
1787
    }
1788

1789
    if (this->tc_cursor.x
6✔
1790
        == (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1791
    {
1792
        if (this->tc_cursor.x >= this->tc_max_cursor_x) {
3✔
1793
            this->tc_max_cursor_x = this->tc_cursor.x;
3✔
1794
        }
1795
    } else {
UNCOV
1796
        this->tc_max_cursor_x = this->tc_cursor.x;
×
1797
    }
1798

1799
    if (orig_top != this->tc_top || orig_left != this->tc_left
3✔
1800
        || orig_cursor != this->tc_cursor
3✔
1801
        || orig_max_cursor_x != this->tc_max_cursor_x)
6✔
1802
    {
1803
        this->set_needs_update();
3✔
1804
    }
1805
}
1806

1807
void
1808
textinput_curses::apply_highlights()
3✔
1809
{
1810
    if (this->tc_text_format == text_format_t::TF_LNAV_SCRIPT) {
3✔
1811
        return;
×
1812
    }
1813

1814
    for (auto& line : this->tc_lines) {
6✔
1815
        for (const auto& hl_pair : this->tc_highlights) {
3✔
1816
            const auto& hl = hl_pair.second;
×
1817

1818
            if (!hl.applies_to_format(this->tc_text_format)) {
×
1819
                continue;
×
1820
            }
UNCOV
1821
            hl.annotate(line, line_range{0, -1});
×
1822
        }
1823
    }
1824
}
1825

1826
std::string
1827
textinput_curses::replace_selection_no_change(string_fragment sf)
3✔
1828
{
1829
    if (!this->tc_selection) {
3✔
UNCOV
1830
        return "";
×
1831
    }
1832

1833
    std::optional<int> del_max;
3✔
1834
    auto full_first_line = false;
3✔
1835
    std::string retval;
3✔
1836

1837
    auto range = std::exchange(this->tc_selection, std::nullopt).value();
3✔
1838
    this->tc_cursor.y = range.sr_start.y;
3✔
1839
    for (auto curr_line = range.sr_start.y;
6✔
1840
         curr_line <= range.sr_end.y && curr_line < this->tc_lines.size();
6✔
1841
         ++curr_line)
1842
    {
1843
        auto sel_range = range.range_for_line(curr_line);
3✔
1844

1845
        if (!sel_range) {
3✔
1846
            continue;
×
1847
        }
1848

1849
        log_debug("sel_range y=%d [%d:%d)",
3✔
1850
                  curr_line,
1851
                  sel_range->lr_start,
1852
                  sel_range->lr_end);
1853
        if (sel_range->lr_start < 0) {
3✔
1854
            if (curr_line > 0) {
×
1855
                log_debug("append %d to %d", curr_line, curr_line - 1);
×
1856
                this->tc_cursor.x
UNCOV
1857
                    = this->tc_lines[curr_line - 1].column_width();
×
1858
                this->tc_cursor.y = curr_line - 1;
×
1859
                this->tc_lines[curr_line - 1].append(this->tc_lines[curr_line]);
×
UNCOV
1860
                retval.push_back('\n');
×
UNCOV
1861
                del_max = curr_line;
×
UNCOV
1862
                full_first_line = true;
×
1863
            }
1864
        } else if (sel_range->lr_start
3✔
1865
                       == (ssize_t) this->tc_lines[curr_line].column_width()
3✔
1866
                   && sel_range->lr_end != -1
3✔
1867
                   && sel_range->lr_start < sel_range->lr_end)
6✔
1868
        {
1869
            // Del deleting line feed
UNCOV
1870
            if (curr_line + 1 < (ssize_t) this->tc_lines.size()) {
×
UNCOV
1871
                this->tc_lines[curr_line].append(this->tc_lines[curr_line + 1]);
×
UNCOV
1872
                retval.push_back('\n');
×
UNCOV
1873
                del_max = curr_line + 1;
×
1874
            }
1875
        } else if (sel_range->lr_start == 0 && sel_range->lr_end == -1) {
3✔
UNCOV
1876
            log_debug("delete full line");
×
UNCOV
1877
            retval.append(this->tc_lines[curr_line].al_string);
×
UNCOV
1878
            retval.push_back('\n');
×
UNCOV
1879
            del_max = curr_line;
×
UNCOV
1880
            if (curr_line == range.sr_start.y) {
×
UNCOV
1881
                log_debug("full first");
×
UNCOV
1882
                full_first_line = true;
×
1883
            }
1884
        } else {
1885
            log_debug("partial line change");
3✔
1886
            auto& al = this->tc_lines[curr_line];
3✔
1887
            auto start = al.column_to_byte_index(sel_range->lr_start);
3✔
1888
            auto end = sel_range->lr_end == -1
3✔
1889
                ? al.al_string.length()
3✔
1890
                : al.column_to_byte_index(sel_range->lr_end);
3✔
1891

1892
            retval.append(al.al_string.substr(start, end - start));
3✔
1893
            if (sel_range->lr_end == -1) {
3✔
UNCOV
1894
                retval.push_back('\n');
×
1895
            }
1896
            al.erase(start, end - start);
3✔
1897
            if (full_first_line || curr_line == range.sr_start.y) {
3✔
1898
                al.insert(start, sf.to_string());
3✔
1899
                this->tc_cursor.x = sel_range->lr_start;
3✔
1900
            }
1901
            if (!full_first_line && sel_range->lr_start == 0
3✔
1902
                && range.sr_start.y < curr_line && curr_line == range.sr_end.y)
6✔
1903
            {
UNCOV
1904
                del_max = curr_line;
×
UNCOV
1905
                this->tc_lines[range.sr_start.y].append(al);
×
1906
            }
1907
        }
1908
    }
1909

1910
    if (del_max) {
3✔
UNCOV
1911
        log_debug("deleting lines [%d+%d:%d)",
×
1912
                  range.sr_start.y,
1913
                  (full_first_line ? 0 : 1),
1914
                  del_max.value() + 1);
UNCOV
1915
        this->tc_lines.erase(this->tc_lines.begin() + range.sr_start.y
×
UNCOV
1916
                                 + (full_first_line ? 0 : 1),
×
UNCOV
1917
                             this->tc_lines.begin() + del_max.value() + 1);
×
1918
    }
1919

1920
    const auto repl_last_line
1921
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'});
3✔
1922
    log_debug(
3✔
1923
        "last line '%.*s'", repl_last_line.length(), repl_last_line.data());
1924
    const auto repl_cols
1925
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'})
3✔
1926
              .column_width();
3✔
1927
    const auto repl_lines = sf.count('\n');
3✔
1928
    log_debug("repl_cols => %zu", repl_cols);
3✔
1929
    if (repl_lines > 0) {
3✔
UNCOV
1930
        this->tc_cursor.x = repl_cols;
×
1931
    } else {
1932
        this->tc_cursor.x += repl_cols;
3✔
1933
    }
1934
    this->tc_cursor.y += repl_lines;
3✔
1935

1936
    this->tc_drag_selection = std::nullopt;
3✔
1937
    if (retval == sf) {
3✔
UNCOV
1938
        if (!sf.empty()) {
×
UNCOV
1939
            this->content_to_lines(this->get_content(),
×
1940
                                   this->get_cursor_offset());
1941
        }
1942
    } else {
1943
        this->update_lines();
3✔
1944
    }
1945

1946
    ensure(!this->tc_lines.empty());
3✔
1947

1948
    return retval;
3✔
1949
}
3✔
1950

1951
void
1952
textinput_curses::replace_selection(string_fragment sf)
3✔
1953
{
1954
    static constexpr uint32_t mask
1955
        = UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N | UC_CATEGORY_MASK_Pc;
1956

1957
    if (!this->tc_selection) {
3✔
UNCOV
1958
        return;
×
1959
    }
1960
    auto range = this->tc_selection.value();
3✔
1961
    auto change_pos = this->tc_change_log.size();
3✔
1962
    auto old_text = this->replace_selection_no_change(sf);
3✔
1963
    if (old_text == sf) {
3✔
UNCOV
1964
        log_trace("no-op replacement");
×
1965
    } else {
1966
        auto is_wordbreak = !sf.empty()
3✔
1967
            && !uc_is_general_category_withtable(sf.front_codepoint(), mask);
3✔
1968
        log_debug("repl sel [%d:%d) - cursor [%d:%d)",
3✔
1969
                  range.sr_start.x,
1970
                  range.sr_start.y,
1971
                  this->tc_cursor.x,
1972
                  this->tc_cursor.y);
1973
        if (this->tc_change_log.empty()
3✔
1974
            || this->tc_change_log.back().ce_range.sr_end != range.sr_start
2✔
1975
            || is_wordbreak)
5✔
1976
        {
1977
            auto redo_range = selected_range::from_key(
1✔
1978
                range.sr_start.x < 0 ? this->tc_cursor : range.sr_start,
1✔
1979
                this->tc_cursor);
1980
            log_debug("  redo range [%d:%d] - [%d:%d]",
1✔
1981
                      redo_range.sr_start.x,
1982
                      redo_range.sr_start.y,
1983
                      redo_range.sr_end.x,
1984
                      redo_range.sr_end.y);
1985
            if (change_pos < this->tc_change_log.size()) {
1✔
1986
                // XXX an on_change handler can run and do its own replacement
1987
                // before we get a change to add or entry
1988
                log_debug("inserting change log at %zu", change_pos);
×
1989
                this->tc_change_log.insert(
×
1990
                    std::next(this->tc_change_log.begin(), change_pos),
×
1991
                    change_entry{redo_range, old_text});
×
1992
            } else {
1993
                this->tc_change_log.emplace_back(redo_range, old_text);
1✔
1994
            }
1995
        } else {
1996
            auto& last_range = this->tc_change_log.back().ce_range;
2✔
1997
            last_range.sr_end = this->tc_cursor;
2✔
1998
            log_debug("extending undo range [%d:%d] - [%d:%d]",
2✔
1999
                      last_range.sr_start.x,
2000
                      last_range.sr_start.y,
2001
                      last_range.sr_end.x,
2002
                      last_range.sr_end.y);
2003
        }
2004
    }
2005
}
3✔
2006

2007
void
UNCOV
2008
textinput_curses::move_cursor_by(movement move)
×
2009
{
UNCOV
2010
    auto cursor_y_offset = this->tc_cursor.y - this->tc_top;
×
UNCOV
2011
    this->tc_cursor += move;
×
UNCOV
2012
    if (move.hm_dir == direction_t::up || move.hm_dir == direction_t::down) {
×
UNCOV
2013
        if (move.hm_amount > 1) {
×
2014
            this->tc_top = this->tc_cursor.y - cursor_y_offset;
×
UNCOV
2015
            this->set_needs_update();
×
2016
        }
UNCOV
2017
        this->tc_cursor.x = this->tc_max_cursor_x;
×
2018
    }
UNCOV
2019
    if (this->tc_cursor.x < 0) {
×
UNCOV
2020
        if (this->tc_cursor.y > 0) {
×
UNCOV
2021
            this->tc_cursor.y -= 1;
×
2022
            this->tc_cursor.x
UNCOV
2023
                = this->tc_lines[this->tc_cursor.y].column_width();
×
2024
        } else {
UNCOV
2025
            this->tc_cursor.x = 0;
×
2026
        }
2027
    }
UNCOV
2028
    if (move.hm_dir == direction_t::right
×
UNCOV
2029
        && this->tc_cursor.x
×
UNCOV
2030
            > (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
×
2031
    {
UNCOV
2032
        if (this->tc_cursor.y + 1 < (ssize_t) this->tc_lines.size()) {
×
UNCOV
2033
            this->tc_cursor.x = 0;
×
UNCOV
2034
            this->tc_cursor.y += 1;
×
UNCOV
2035
            this->tc_max_cursor_x = 0;
×
2036
        }
2037
    }
UNCOV
2038
    this->clamp_point(this->tc_cursor);
×
UNCOV
2039
    if (this->tc_drag_selection) {
×
UNCOV
2040
        this->tc_drag_selection = std::nullopt;
×
UNCOV
2041
        this->set_needs_update();
×
2042
    }
UNCOV
2043
    if (this->tc_selection) {
×
UNCOV
2044
        this->tc_selection = std::nullopt;
×
UNCOV
2045
        this->set_needs_update();
×
2046
    }
UNCOV
2047
    this->ensure_cursor_visible();
×
2048
}
2049

2050
void
2051
textinput_curses::move_cursor_to(input_point ip)
2✔
2052
{
2053
    this->tc_cursor = ip;
2✔
2054
    if (this->tc_drag_selection) {
2✔
2055
        this->tc_drag_selection = std::nullopt;
×
UNCOV
2056
        this->set_needs_update();
×
2057
    }
2058
    if (this->tc_selection) {
2✔
UNCOV
2059
        this->tc_selection = std::nullopt;
×
UNCOV
2060
        this->set_needs_update();
×
2061
    }
2062
    this->ensure_cursor_visible();
2✔
2063
}
2✔
2064

2065
void
2066
textinput_curses::update_lines()
3✔
2067
{
2068
    const auto x = this->get_cursor_offset();
3✔
2069
    this->content_to_lines(this->get_content(), x);
3✔
2070
    this->set_needs_update();
3✔
2071
    this->ensure_cursor_visible();
3✔
2072

2073
    this->tc_marks.clear();
3✔
2074
    if (this->tc_in_popup_change) {
3✔
UNCOV
2075
        log_trace("in popup change, skipping");
×
2076
    } else {
2077
        this->tc_popup.set_visible(false);
3✔
2078
        this->tc_complete_range = std::nullopt;
3✔
2079
        if (this->tc_on_change) {
3✔
2080
            this->tc_on_change(*this);
3✔
2081
        }
2082
        if (!this->tc_popup.is_visible()) {
3✔
2083
            this->tc_popup_type = popup_type_t::none;
3✔
2084
        }
2085
    }
2086

2087
    ensure(!this->tc_lines.empty());
3✔
2088
}
3✔
2089

2090
textinput_curses::dimension_result
2091
textinput_curses::get_visible_dimensions() const
23✔
2092
{
2093
    dimension_result retval;
23✔
2094

2095
    ncplane_dim_yx(
23✔
2096
        this->tc_window, &retval.dr_full_height, &retval.dr_full_width);
23✔
2097

2098
    if (this->vc_y < (ssize_t) retval.dr_full_height) {
23✔
2099
        retval.dr_height = std::min((int) retval.dr_full_height - this->vc_y,
23✔
2100
                                    this->tc_height);
23✔
2101
    }
2102
    if (this->vc_x < (ssize_t) retval.dr_full_width) {
23✔
2103
        retval.dr_width = std::min((long) retval.dr_full_width - this->vc_x,
23✔
2104
                                   this->vc_width);
23✔
2105
    }
2106
    return retval;
23✔
2107
}
2108

2109
std::string
2110
textinput_curses::get_content(bool trim) const
7✔
2111
{
2112
    auto need_lf = false;
7✔
2113
    std::string retval;
7✔
2114

2115
    for (const auto& al : this->tc_lines) {
14✔
2116
        const auto& line = al.al_string;
7✔
2117
        auto line_sf = string_fragment::from_str(line);
7✔
2118
        if (trim) {
7✔
UNCOV
2119
            line_sf = line_sf.rtrim(" ");
×
2120
        }
2121
        if (need_lf) {
7✔
2122
            retval.push_back('\n');
×
2123
        }
2124
        retval += line_sf;
7✔
2125
        need_lf = true;
7✔
2126
    }
2127
    return retval;
7✔
UNCOV
2128
}
×
2129

2130
void
2131
textinput_curses::focus()
3✔
2132
{
2133
    if (!this->vc_enabled) {
3✔
2134
        this->vc_enabled = true;
1✔
2135
        if (this->tc_on_focus) {
1✔
UNCOV
2136
            this->tc_on_focus(*this);
×
2137
        }
2138
        this->set_needs_update();
1✔
2139
    }
2140

2141
    if (this->tc_mode == mode_t::show_help
6✔
2142
        || (this->tc_height && this->tc_notice)
3✔
2143
        || (this->tc_selection
6✔
2144
            && this->tc_selection->contains_exclusive(this->tc_cursor)))
3✔
2145
    {
2146
        notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
×
UNCOV
2147
        return;
×
2148
    }
2149
    auto term_x = this->vc_x + this->tc_cursor.x - this->tc_left;
3✔
2150
    if (this->tc_cursor.y == 0) {
3✔
2151
        term_x += this->tc_prefix.column_width();
2✔
2152
    }
2153
    notcurses_cursor_enable(ncplane_notcurses(this->tc_window),
3✔
2154
                            this->vc_y + this->tc_cursor.y - this->tc_top,
3✔
2155
                            term_x);
2156
}
2157

2158
void
2159
textinput_curses::blur()
1✔
2160
{
2161
    this->tc_popup_type = popup_type_t::none;
1✔
2162
    this->tc_popup.set_visible(false);
1✔
2163
    this->vc_enabled = false;
1✔
2164
    this->move_cursor_to(input_point::home());
1✔
2165
    if (this->tc_on_blur) {
1✔
2166
        this->tc_on_blur(*this);
1✔
2167
    }
2168

2169
    notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
1✔
2170
    this->set_needs_update();
1✔
2171
}
1✔
2172

2173
void
2174
textinput_curses::abort()
×
2175
{
UNCOV
2176
    this->blur();
×
UNCOV
2177
    this->tc_selection = std::nullopt;
×
UNCOV
2178
    this->tc_drag_selection = std::nullopt;
×
2179
    if (this->tc_on_abort) {
×
UNCOV
2180
        this->tc_on_abort(*this);
×
2181
    }
2182
}
2183

2184
void
2185
textinput_curses::sync_to_sysclip() const
×
2186
{
2187
    auto clip_open_res = sysclip::open(sysclip::type_t::GENERAL);
×
2188

2189
    if (clip_open_res.isOk()) {
×
2190
        auto clip_file = clip_open_res.unwrap();
×
2191
        fmt::print(clip_file.in(),
×
UNCOV
2192
                   FMT_STRING("{}"),
×
UNCOV
2193
                   fmt::join(this->tc_clipboard, ""));
×
UNCOV
2194
    } else {
×
UNCOV
2195
        auto err_msg = clip_open_res.unwrapErr();
×
UNCOV
2196
        log_error("unable to open clipboard: %s", err_msg.c_str());
×
2197
    }
2198
}
2199

2200
bool
2201
textinput_curses::do_update()
24✔
2202
{
2203
    static auto& vc = view_colors::singleton();
24✔
2204
    auto retval = false;
24✔
2205

2206
    if (!this->is_visible()) {
24✔
2207
        return retval;
1✔
2208
    }
2209

2210
    auto popup_height = this->tc_popup.get_height();
23✔
2211
    auto rel_y = (this->tc_popup_type == popup_type_t::history
23✔
2212
                      ? 0
23✔
2213
                      : this->tc_cursor.y - this->tc_top)
23✔
2214
        - popup_height;
23✔
2215
    if (this->vc_y + rel_y < 0) {
23✔
UNCOV
2216
        rel_y = this->tc_cursor.y - this->tc_top + popup_height + 1;
×
2217
    }
2218
    this->tc_popup.set_y(this->vc_y + rel_y);
23✔
2219

2220
    if (!this->vc_needs_update) {
23✔
2221
        return view_curses::do_update();
7✔
2222
    }
2223

2224
    auto dim = this->get_visible_dimensions();
16✔
2225
    if (!this->vc_enabled) {
16✔
2226
        ncplane_erase_region(
15✔
2227
            this->tc_window, this->vc_y, this->vc_x, 1, dim.dr_width);
2228
        auto lr = line_range{0, dim.dr_width};
15✔
UNCOV
2229
        mvwattrline(this->tc_window,
×
2230
                    this->vc_y,
2231
                    this->vc_x,
2232
                    this->tc_inactive_value,
15✔
2233
                    lr);
2234

2235
        if (!this->tc_alt_value.empty()
15✔
2236
            && (ssize_t) (this->tc_inactive_value.column_width() + 3
19✔
2237
                          + this->tc_alt_value.column_width())
4✔
2238
                < dim.dr_width)
4✔
2239
        {
2240
            auto alt_x = dim.dr_width - this->tc_alt_value.column_width();
4✔
2241
            auto lr = line_range{0, (int) this->tc_alt_value.column_width()};
4✔
2242
            mvwattrline(
4✔
2243
                this->tc_window, this->vc_y, alt_x, this->tc_alt_value, lr);
4✔
2244
        }
2245

2246
        this->vc_needs_update = false;
15✔
2247
        return true;
15✔
2248
    }
2249

2250
    if (this->tc_mode == mode_t::show_help) {
1✔
2251
        this->tc_help_view.set_window(this->tc_window);
×
2252
        this->tc_help_view.set_x(this->vc_x);
×
2253
        this->tc_help_view.set_y(this->vc_y);
×
2254
        this->tc_help_view.set_width(this->vc_width);
×
UNCOV
2255
        this->tc_help_view.set_height(vis_line_t(this->tc_height));
×
2256
        this->tc_help_view.set_visible(true);
×
2257
        return view_curses::do_update();
×
2258
    }
2259

2260
    retval = true;
1✔
2261
    ssize_t row_count = this->tc_lines.size();
1✔
2262
    auto y = this->vc_y;
1✔
2263
    auto y_max = this->vc_y + dim.dr_height;
1✔
2264
    if (row_count == 1 && this->tc_lines[0].empty()
1✔
2265
        && !this->tc_suggestion.empty())
2✔
2266
    {
2267
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
UNCOV
2268
        auto al = attr_line_t(this->tc_suggestion)
×
UNCOV
2269
                      .with_attr_for_all(VC_ROLE.value(role_t::VCR_SUGGESTION));
×
UNCOV
2270
        al.insert(0, this->tc_prefix);
×
2271
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
×
2272
        mvwattrline(this->tc_window, y, this->vc_x, al, lr);
×
2273
        row_count -= 1;
×
2274
        y += 1;
×
2275
    }
2276
    auto abort_msg_shown = false;
1✔
2277
    for (auto curr_line = this->tc_top; curr_line < row_count && y < y_max;
2✔
2278
         curr_line++, y++)
1✔
2279
    {
2280
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
1✔
2281
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
1✔
2282
        auto al = this->tc_lines[curr_line];
1✔
2283
        if (this->tc_drag_selection) {
1✔
UNCOV
2284
            auto sel_lr = this->tc_drag_selection->range_for_line(curr_line);
×
UNCOV
2285
            if (sel_lr) {
×
2286
                al.al_attrs.emplace_back(
×
UNCOV
2287
                    sel_lr.value(), VC_ROLE.value(role_t::VCR_SELECTED_TEXT));
×
2288
            }
2289
        } else if (this->tc_selection) {
1✔
2290
            auto sel_lr = this->tc_selection->range_for_line(curr_line);
×
UNCOV
2291
            if (sel_lr) {
×
UNCOV
2292
                al.al_attrs.emplace_back(
×
2293
                    sel_lr.value(), VC_STYLE.value(text_attrs::with_reverse()));
×
2294
            }
2295
        }
2296
        if (this->tc_mode == mode_t::searching
2✔
2297
            && this->tc_search_found.value_or(false))
1✔
2298
        {
2299
            this->tc_search_code->capture_from(al.al_string)
×
UNCOV
2300
                .for_each([&al](lnav::pcre2pp::match_data& md) {
×
2301
                    al.al_attrs.emplace_back(
×
2302
                        line_range{
×
UNCOV
2303
                            md[0]->sf_begin,
×
UNCOV
2304
                            md[0]->sf_end,
×
2305
                        },
UNCOV
2306
                        VC_ROLE.value(role_t::VCR_SEARCH));
×
UNCOV
2307
                });
×
2308
        }
2309
        if (!this->tc_suggestion.empty() && !this->tc_popup.is_visible()
1✔
2310
            && curr_line == this->tc_cursor.y
×
2311
            && this->tc_cursor.x == (ssize_t) al.column_width())
1✔
2312
        {
2313
            al.append(this->tc_suggestion,
×
2314
                      VC_ROLE.value(role_t::VCR_SUGGESTION));
×
2315
        }
2316
        if (curr_line == 0) {
1✔
2317
            al.insert(0, this->tc_prefix);
1✔
2318
        }
2319
        mvwattrline(this->tc_window, y, this->vc_x, al, lr);
1✔
2320

2321
        if (!abort_msg_shown && this->tc_abort_requested) {
1✔
2322
            static auto REQ_MSG
2323
                = attr_line_t("  Press ")
×
2324
                      .append("Esc"_hotkey)
×
UNCOV
2325
                      .append(" to abort  ")
×
2326
                      .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
UNCOV
2327
            auto msg_lr = line_range{0, 0 + dim.dr_width};
×
UNCOV
2328
            mvwattrline(
×
2329
                this->tc_window,
2330
                y,
UNCOV
2331
                this->vc_x + dim.dr_width - REQ_MSG.utf8_length_or_length(),
×
2332
                REQ_MSG,
2333
                msg_lr);
UNCOV
2334
            abort_msg_shown = true;
×
2335
        }
2336
    }
1✔
2337
    for (; y < y_max; y++) {
1✔
2338
        static constexpr auto EMPTY_LR = line_range::empty_at(0);
2339

UNCOV
2340
        auto al = attr_line_t();
×
UNCOV
2341
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
2342
        mvwattrline(
×
2343
            this->tc_window, y, this->vc_x, al, EMPTY_LR, role_t::VCR_ALT_ROW);
2344
    }
2345
    if (this->tc_notice) {
1✔
UNCOV
2346
        auto notice_lines = this->tc_notice.value();
×
UNCOV
2347
        auto avail_height = std::min(dim.dr_height, (int) notice_lines.size());
×
UNCOV
2348
        auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2349

UNCOV
2350
        for (auto& al : notice_lines) {
×
UNCOV
2351
            auto lr = line_range{0, dim.dr_width};
×
UNCOV
2352
            mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
UNCOV
2353
            if (notice_y >= y_max) {
×
2354
                break;
×
2355
            }
2356
        }
2357
    } else if (this->tc_mode == mode_t::searching) {
1✔
UNCOV
2358
        auto search_prompt = attr_line_t(" ");
×
2359
        if (this->tc_search.empty() || this->tc_search_found.has_value()) {
×
UNCOV
2360
            search_prompt.append(this->tc_search)
×
UNCOV
2361
                .append(" ", VC_ROLE.value(role_t::VCR_CURSOR_LINE));
×
2362
        } else {
UNCOV
2363
            search_prompt.append(this->tc_search,
×
UNCOV
2364
                                 VC_ROLE.value(role_t::VCR_SEARCH));
×
2365
        }
UNCOV
2366
        if (this->tc_search_found && this->tc_search_found.value()) {
×
2367
            search_prompt.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2368
        }
2369
        search_prompt.insert(0, " Search: "_status_subtitle);
×
2370
        if (this->tc_search_found && !this->tc_search_found.value()) {
×
2371
            search_prompt.with_attr_for_all(
×
2372
                VC_ROLE.value(role_t::VCR_ALERT_STATUS));
×
2373
        }
UNCOV
2374
        auto lr = line_range{0, dim.dr_width};
×
2375
        mvwattrline(this->tc_window,
×
2376
                    this->vc_y + dim.dr_height - 1,
×
2377
                    this->vc_x,
2378
                    search_prompt,
2379
                    lr);
2380
    } else if (this->tc_height > 1) {
1✔
UNCOV
2381
        auto mark_iter = this->tc_marks.find(this->tc_cursor);
×
2382

UNCOV
2383
        if (mark_iter != this->tc_marks.end()) {
×
UNCOV
2384
            auto mark_lines = mark_iter->second.to_attr_line().split_lines();
×
2385
            auto avail_height
2386
                = std::min(dim.dr_height, (int) mark_lines.size());
×
UNCOV
2387
            auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
UNCOV
2388
            for (auto& al : mark_lines) {
×
2389
                al.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2390
                auto lr = line_range{0, dim.dr_width};
×
2391
                mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
UNCOV
2392
                if (notice_y >= y_max) {
×
UNCOV
2393
                    break;
×
2394
                }
2395
            }
2396
        }
2397
    }
2398

2399
    if (this->tc_height > 1) {
1✔
2400
        double progress = 1.0;
×
2401
        double coverage = 1.0;
×
2402

UNCOV
2403
        if (row_count > 0) {
×
2404
            progress = (double) this->tc_top / (double) row_count;
×
UNCOV
2405
            coverage = (double) dim.dr_height / (double) row_count;
×
2406
        }
2407

UNCOV
2408
        auto scroll_top = (int) (progress * (double) dim.dr_height);
×
2409
        auto scroll_bottom = scroll_top
UNCOV
2410
            + std::min(dim.dr_height,
×
UNCOV
2411
                       (int) (coverage * (double) dim.dr_height));
×
2412

UNCOV
2413
        for (auto y = this->vc_y; y < y_max; y++) {
×
UNCOV
2414
            auto role = this->vc_default_role;
×
UNCOV
2415
            auto bar_role = role_t::VCR_SCROLLBAR;
×
UNCOV
2416
            auto ch = NCACS_VLINE;
×
UNCOV
2417
            if (y >= this->vc_y + scroll_top && y <= this->vc_y + scroll_bottom)
×
2418
            {
UNCOV
2419
                role = bar_role;
×
2420
            }
UNCOV
2421
            auto attrs = vc.attrs_for_role(role);
×
UNCOV
2422
            ncplane_putstr_yx(
×
UNCOV
2423
                this->tc_window, y, this->vc_x + dim.dr_width - 1, ch);
×
UNCOV
2424
            ncplane_set_cell_yx(this->tc_window,
×
2425
                                y,
UNCOV
2426
                                this->vc_x + dim.dr_width - 1,
×
UNCOV
2427
                                attrs.ta_attrs | NCSTYLE_ALTCHARSET,
×
2428
                                view_colors::to_channels(attrs));
2429
        }
2430
    }
2431

2432
    return view_curses::do_update() || retval;
1✔
2433
}
2434

2435
void
UNCOV
2436
textinput_curses::open_popup_for_completion(
×
2437
    line_range crange, std::vector<attr_line_t> possibilities)
2438
{
UNCOV
2439
    if (possibilities.empty()) {
×
UNCOV
2440
        this->tc_popup_type = popup_type_t::none;
×
UNCOV
2441
        return;
×
2442
    }
2443

UNCOV
2444
    this->tc_popup_type = popup_type_t::completion;
×
UNCOV
2445
    auto dim = this->get_visible_dimensions();
×
2446
    auto max_width = possibilities
UNCOV
2447
        | lnav::itertools::map(&attr_line_t::column_width)
×
UNCOV
2448
        | lnav::itertools::max();
×
2449

UNCOV
2450
    auto full_width = std::min((int) max_width.value_or(1) + 3, dim.dr_width);
×
UNCOV
2451
    auto new_sel = 0_vl;
×
2452
    auto popup_height = vis_line_t(
UNCOV
2453
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
UNCOV
2454
    ssize_t rel_x = crange.lr_start;
×
UNCOV
2455
    if (this->tc_cursor.y == 0) {
×
UNCOV
2456
        rel_x += this->tc_prefix.column_width();
×
2457
    }
UNCOV
2458
    if (rel_x + full_width > dim.dr_width) {
×
UNCOV
2459
        rel_x = dim.dr_width - full_width;
×
2460
    }
UNCOV
2461
    if (this->vc_x + rel_x > 0) {
×
UNCOV
2462
        rel_x -= 1;  // XXX for border
×
2463
    }
UNCOV
2464
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
UNCOV
2465
    if (this->vc_y + rel_y < 0) {
×
UNCOV
2466
        rel_y = this->tc_cursor.y - this->tc_top + 1;
×
2467
    } else {
UNCOV
2468
        std::reverse(possibilities.begin(), possibilities.end());
×
UNCOV
2469
        new_sel = vis_line_t(possibilities.size() - 1);
×
2470
    }
2471

2472
    this->tc_complete_range
UNCOV
2473
        = selected_range::from_key(this->tc_cursor.copy_with_x(crange.lr_start),
×
UNCOV
2474
                                   this->tc_cursor.copy_with_x(crange.lr_end));
×
UNCOV
2475
    this->tc_popup_source.replace_with(possibilities);
×
UNCOV
2476
    this->tc_popup.set_window(this->tc_window);
×
UNCOV
2477
    this->tc_popup.set_x(this->vc_x + rel_x);
×
UNCOV
2478
    this->tc_popup.set_y(this->vc_y + rel_y);
×
UNCOV
2479
    this->tc_popup.set_width(full_width);
×
UNCOV
2480
    this->tc_popup.set_height(popup_height);
×
UNCOV
2481
    this->tc_popup.set_visible(true);
×
UNCOV
2482
    this->tc_popup.set_top(0_vl);
×
UNCOV
2483
    this->tc_popup.set_selection(new_sel);
×
UNCOV
2484
    this->set_needs_update();
×
2485
}
2486

2487
void
UNCOV
2488
textinput_curses::open_popup_for_history(std::vector<attr_line_t> possibilities)
×
2489
{
UNCOV
2490
    if (possibilities.empty()) {
×
UNCOV
2491
        this->tc_popup_type = popup_type_t::none;
×
UNCOV
2492
        return;
×
2493
    }
2494

UNCOV
2495
    this->tc_popup_type = popup_type_t::history;
×
UNCOV
2496
    auto new_sel = 0_vl;
×
2497
    auto popup_height = vis_line_t(
UNCOV
2498
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
UNCOV
2499
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
UNCOV
2500
    if (this->vc_y + rel_y < 0) {
×
UNCOV
2501
        rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2502
    } else {
UNCOV
2503
        std::reverse(possibilities.begin(), possibilities.end());
×
UNCOV
2504
        new_sel = vis_line_t(possibilities.size() - 1);
×
2505
    }
2506

UNCOV
2507
    this->tc_complete_range = selected_range::from_key(
×
2508
        input_point::home(),
2509
        input_point{
UNCOV
2510
            (int) this->tc_lines.back().column_width(),
×
UNCOV
2511
            (int) this->tc_lines.size() - 1,
×
UNCOV
2512
        });
×
UNCOV
2513
    this->tc_popup_source.replace_with(possibilities);
×
UNCOV
2514
    this->tc_popup.set_window(this->tc_window);
×
UNCOV
2515
    this->tc_popup.set_title("History");
×
UNCOV
2516
    this->tc_popup.set_x(this->vc_x);
×
UNCOV
2517
    this->tc_popup.set_y(this->vc_y + rel_y);
×
UNCOV
2518
    this->tc_popup.set_width(this->vc_width);
×
UNCOV
2519
    this->tc_popup.set_height(popup_height);
×
UNCOV
2520
    this->tc_popup.set_top(0_vl);
×
UNCOV
2521
    this->tc_popup.set_selection(new_sel);
×
UNCOV
2522
    this->tc_popup.set_visible(true);
×
UNCOV
2523
    if (this->tc_on_popup_change) {
×
UNCOV
2524
        this->tc_in_popup_change = true;
×
UNCOV
2525
        this->tc_on_popup_change(*this);
×
UNCOV
2526
        this->tc_in_popup_change = false;
×
2527
    }
UNCOV
2528
    this->set_needs_update();
×
2529
}
2530

2531
void
UNCOV
2532
textinput_curses::tick(ui_clock::time_point now)
×
2533
{
UNCOV
2534
    if (this->tc_last_tick_after_input) {
×
UNCOV
2535
        auto diff = now - this->tc_last_tick_after_input.value();
×
2536

UNCOV
2537
        if (diff >= 750ms && !this->tc_timeout_fired) {
×
UNCOV
2538
            if (this->tc_on_timeout) {
×
UNCOV
2539
                this->tc_on_timeout(*this);
×
2540
            }
UNCOV
2541
            this->tc_timeout_fired = true;
×
2542
        }
2543
    } else {
UNCOV
2544
        this->tc_last_tick_after_input = now;
×
UNCOV
2545
        this->tc_timeout_fired = false;
×
2546
    }
2547
}
2548

2549
int
2550
textinput_curses::get_cursor_offset() const
3✔
2551
{
2552
    if (this->tc_cursor.y < 0
6✔
2553
        || this->tc_cursor.y >= (ssize_t) this->tc_lines.size())
3✔
2554
    {
2555
        // XXX can happen during update_lines() with history/pasted insert
UNCOV
2556
        return 0;
×
2557
    }
2558

2559
    int retval = 0;
3✔
2560
    for (auto row = 0; row < this->tc_cursor.y; row++) {
3✔
UNCOV
2561
        retval += this->tc_lines[row].al_string.size() + 1;
×
2562
    }
2563
    retval += this->tc_cursor.x;
3✔
2564

2565
    return retval;
3✔
2566
}
2567

2568
textinput_curses::input_point
UNCOV
2569
textinput_curses::get_point_for_offset(int offset) const
×
2570
{
UNCOV
2571
    auto retval = input_point::home();
×
UNCOV
2572
    auto row = size_t{0};
×
UNCOV
2573
    for (; row < this->tc_lines.size() && offset > 0; row++) {
×
UNCOV
2574
        if (offset < (ssize_t) this->tc_lines[row].al_string.size() + 1) {
×
UNCOV
2575
            break;
×
2576
        }
UNCOV
2577
        offset -= this->tc_lines[row].al_string.size() + 1;
×
UNCOV
2578
        retval.y += 1;
×
2579
    }
UNCOV
2580
    if (row < this->tc_lines.size()) {
×
UNCOV
2581
        retval.x = this->tc_lines[row].byte_to_column_index(offset);
×
2582
    }
2583

UNCOV
2584
    return retval;
×
2585
}
2586

2587
void
UNCOV
2588
textinput_curses::add_mark(input_point pos,
×
2589
                           const lnav::console::user_message& msg)
2590
{
UNCOV
2591
    if (pos.y < 0 || pos.y >= (ssize_t) this->tc_lines.size()) {
×
UNCOV
2592
        log_error("invalid mark position: %d:%d", pos.x, pos.y);
×
UNCOV
2593
        return;
×
2594
    }
2595

UNCOV
2596
    if (this->tc_marks.count(pos) > 0) {
×
UNCOV
2597
        return;
×
2598
    }
2599

UNCOV
2600
    auto& line = this->tc_lines[pos.y];
×
UNCOV
2601
    auto byte_x = (int) line.column_to_byte_index(pos.x);
×
UNCOV
2602
    auto lr = line_range{byte_x, byte_x + 1};
×
UNCOV
2603
    line.al_attrs.emplace_back(lr, VC_ROLE.value(role_t::VCR_ERROR));
×
UNCOV
2604
    line.al_attrs.emplace_back(lr, VC_STYLE.value(text_attrs::with_reverse()));
×
2605

UNCOV
2606
    this->tc_marks.emplace(pos, msg);
×
2607
}
2608

2609
void
UNCOV
2610
textinput_curses::undo_last_change()
×
2611
{
UNCOV
2612
    if (this->tc_change_log.empty()) {
×
UNCOV
2613
        this->tc_notice = no_changes();
×
UNCOV
2614
        this->set_needs_update();
×
2615
    } else {
UNCOV
2616
        log_debug("undo!");
×
UNCOV
2617
        const auto& ce = this->tc_change_log.back();
×
UNCOV
2618
        auto content_sf = string_fragment::from_str(ce.ce_content);
×
UNCOV
2619
        this->tc_selection = ce.ce_range;
×
UNCOV
2620
        log_debug(" range [%d:%d) - [%d:%d) - %s",
×
2621
                  this->tc_selection->sr_start.x,
2622
                  this->tc_selection->sr_start.y,
2623
                  this->tc_selection->sr_end.x,
2624
                  this->tc_selection->sr_end.y,
2625
                  ce.ce_content.c_str());
UNCOV
2626
        this->replace_selection_no_change(content_sf);
×
UNCOV
2627
        this->tc_change_log.pop_back();
×
2628
    }
2629
}
2630

2631
std::string
UNCOV
2632
textinput_curses::selection_to_string() const
×
2633
{
UNCOV
2634
    std::string retval;
×
2635

UNCOV
2636
    if (!this->tc_selection) {
×
UNCOV
2637
        return retval;
×
2638
    }
UNCOV
2639
    auto range = this->tc_selection;
×
UNCOV
2640
    auto add_nl = false;
×
UNCOV
2641
    for (auto y = range->sr_start.y;
×
UNCOV
2642
         y <= range->sr_end.y && y < this->tc_lines.size();
×
2643
         ++y)
2644
    {
UNCOV
2645
        if (add_nl) {
×
UNCOV
2646
            retval.push_back('\n');
×
2647
        }
UNCOV
2648
        auto sel_range = range->range_for_line(y);
×
UNCOV
2649
        if (!sel_range) {
×
UNCOV
2650
            continue;
×
2651
        }
2652

UNCOV
2653
        const auto& al = this->tc_lines[y];
×
UNCOV
2654
        auto byte_start = al.column_to_byte_index(sel_range->lr_start);
×
UNCOV
2655
        auto byte_end = al.column_to_byte_index(sel_range->lr_end);
×
UNCOV
2656
        auto al_sf = string_fragment::from_str_range(
×
UNCOV
2657
            al.al_string, byte_start, byte_end);
×
UNCOV
2658
        retval += al_sf;
×
UNCOV
2659
        add_nl = true;
×
2660
    }
2661

UNCOV
2662
    return retval;
×
UNCOV
2663
}
×
2664

2665
void
UNCOV
2666
textinput_curses::copy_selection()
×
2667
{
UNCOV
2668
    if (!this->tc_selection) {
×
UNCOV
2669
        return;
×
2670
    }
2671

UNCOV
2672
    auto content = this->selection_to_string();
×
UNCOV
2673
    this->tc_clipboard.clear();
×
UNCOV
2674
    this->tc_cut_location = this->tc_cursor;
×
UNCOV
2675
    this->tc_clipboard.emplace_back(content);
×
UNCOV
2676
    this->sync_to_sysclip();
×
2677
}
2678

2679
void
UNCOV
2680
textinput_curses::cut_selection()
×
2681
{
UNCOV
2682
    if (!this->tc_selection) {
×
UNCOV
2683
        return;
×
2684
    }
2685

UNCOV
2686
    auto range = this->tc_selection;
×
UNCOV
2687
    log_debug("cutting selection [%d:%d) - [%d:%d)",
×
2688
              range->sr_start.x,
2689
              range->sr_start.y,
2690
              range->sr_end.x,
2691
              range->sr_end.y);
UNCOV
2692
    auto new_clip = this->selection_to_string();
×
UNCOV
2693
    this->tc_clipboard.clear();
×
UNCOV
2694
    this->tc_clipboard.emplace_back(new_clip);
×
UNCOV
2695
    this->replace_selection(string_fragment{});
×
UNCOV
2696
    this->sync_to_sysclip();
×
2697
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc