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

tstack / lnav / 19870100498-2728

02 Dec 2025 06:56PM UTC coverage: 68.872% (+0.04%) from 68.835%
19870100498-2728

push

github

tstack
[timeline] add files and threads

118 of 126 new or added lines in 9 files covered. (93.65%)

458 existing lines in 5 files now uncovered.

51405 of 74639 relevant lines covered (68.87%)

435136.79 hits per line

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

28.25
/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 "unictype.h"
45
#include "ww898/cp_utf8.hpp"
46

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

50
const attr_line_t&
51
textinput_curses::get_help_text()
33✔
52
{
53
    static const auto retval
54
        = attr_line_t()
31✔
55
              .append("Prompt Help"_h1)
31✔
56
              .append("\n\n")
31✔
57
              .append("Editing"_h2)
31✔
58
              .append("\n ")
31✔
59
              .append("\u2022"_list_glyph)
31✔
60
              .append(" ")
31✔
61
              .append("ESC"_hotkey)
31✔
62
              .append("       - Cancel editing\n ")
31✔
63
              .append("\u2022"_list_glyph)
31✔
64
              .append(" ")
31✔
65
              .append("CTRL-X"_hotkey)
31✔
66
              .append("    - Save and exit the editor\n ")
31✔
67
              .append("\u2022"_list_glyph)
31✔
68
              .append(" ")
31✔
69
              .append("HOME"_hotkey)
31✔
70
              .append("      - Move to the beginning of the buffer\n ")
31✔
71
              .append("\u2022"_list_glyph)
31✔
72
              .append(" ")
31✔
73
              .append("END"_hotkey)
31✔
74
              .append("       - Move to the end of the buffer\n ")
31✔
75
              .append("\u2022"_list_glyph)
31✔
76
              .append(" ")
31✔
77
              .append("CTRL-A"_hotkey)
31✔
78
              .append("    - Move to the beginning of the line\n ")
31✔
79
              .append("\u2022"_list_glyph)
31✔
80
              .append(" ")
31✔
81
              .append("CTRL-E"_hotkey)
31✔
82
              .append("    - Move to the end of the line\n ")
31✔
83
              .append("\u2022"_list_glyph)
31✔
84
              .append(" ")
31✔
85
              .append("CTRL-N"_hotkey)
31✔
86
              .append(
31✔
87
                  "    - Move down one line.  If a popup is open, move the "
88
                  "selection down.\n ")
89
              .append("\u2022"_list_glyph)
31✔
90
              .append(" ")
31✔
91
              .append("CTRL-P"_hotkey)
31✔
92
              .append(
31✔
93
                  "    - Move up one line.  If a popup is open, move the "
94
                  "selection up.\n ")
95
              .append("\u2022"_list_glyph)
31✔
96
              .append(" ")
31✔
97
              .append("ALT  \u2190"_hotkey)
31✔
98
              .append("    - Move to the previous word\n ")
31✔
99
              .append("\u2022"_list_glyph)
31✔
100
              .append(" ")
31✔
101
              .append("ALT  \u2192"_hotkey)
31✔
102
              .append("    - Move to the end of the line\n ")
31✔
103
              .append("\u2022"_list_glyph)
31✔
104
              .append(" ")
31✔
105
              .append("CTRL-K"_hotkey)
31✔
106
              .append("    - Cut to the end of the line into the clipboard\n ")
31✔
107
              .append("\u2022"_list_glyph)
31✔
108
              .append(" ")
31✔
109
              .append("CTRL-U"_hotkey)
31✔
110
              .append(
31✔
111
                  "    - Cut from the beginning of the line to the cursor "
112
                  "into the clipboard\n ")
113
              .append("\u2022"_list_glyph)
31✔
114
              .append(" ")
31✔
115
              .append("CTRL-W"_hotkey)
31✔
116
              .append(
31✔
117
                  "    - Cut from the beginning of the previous word into "
118
                  "the clipboard\n ")
119
              .append("\u2022"_list_glyph)
31✔
120
              .append(" ")
31✔
121
              .append("Rt-click"_hotkey)
31✔
122
              .append("  - Copy selection to the system clipboard\n ")
31✔
123
              .append("\u2022"_list_glyph)
31✔
124
              .append(" ")
31✔
125
              .append("CTRL-Y"_hotkey)
31✔
126
              .append("    - Paste the clipboard content\n ")
31✔
127
              .append("\u2022"_list_glyph)
31✔
128
              .append(" ")
31✔
129
              .append("TAB/ENTER"_hotkey)
31✔
130
              .append(" - Accept a completion suggestion\n ")
31✔
131
              .append("\u2022"_list_glyph)
31✔
132
              .append(" ")
31✔
133
              .append("CTRL-_"_hotkey)
31✔
134
              .append("    - Undo a change\n ")
31✔
135
              .append("\u2022"_list_glyph)
31✔
136
              .append(" ")
31✔
137
              .append("CTRL-L"_hotkey)
31✔
138
              .append("    - Reformat the contents, if available\n ")
31✔
139
              .append("\u2022"_list_glyph)
31✔
140
              .append(" ")
31✔
141
              .append("CTRL-O"_hotkey)
31✔
142
              .append("    - Open the contents in an external editor\n")
31✔
143
              .append("\n")
31✔
144
              .append("History"_h2)
31✔
145
              .append("\n ")
31✔
146
              .append("\u2022"_list_glyph)
31✔
147
              .append(" ")
31✔
148
              .append("\u2191"_hotkey)
31✔
149
              .append("      - Select content from the history\n ")
31✔
150
              .append("\u2022"_list_glyph)
31✔
151
              .append(" ")
31✔
152
              .append("CTRL-R"_hotkey)
31✔
153
              .append(" - Search history using current contents\n")
31✔
154
              .append("\n")
31✔
155
              .append("Searching"_h2)
31✔
156
              .append("\n ")
31✔
157
              .append("\u2022"_list_glyph)
31✔
158
              .append(" ")
31✔
159
              .append("CTRL-S"_hotkey)
31✔
160
              .append(" - Switch to search mode\n ")
31✔
161
              .append("\u2022"_list_glyph)
31✔
162
              .append(" ")
31✔
163
              .append("CTRL-R"_hotkey)
31✔
164
              .append(" - Search backwards for the string\n");
64✔
165

166
    return retval;
33✔
167
}
168

169
const std::vector<attr_line_t>&
170
textinput_curses::unhandled_input()
×
171
{
172
    static const auto retval = std::vector{
173
        attr_line_t()
×
174
            .append(" Notice: "_status_subtitle)
×
175
            .append(" Unhandled key press.  Press F1 for help")
×
176
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_ALERT_STATUS)),
×
177
    };
178

179
    return retval;
×
180
}
181

182
const std::vector<attr_line_t>&
183
textinput_curses::no_changes()
×
184
{
185
    static const auto retval = std::vector{
186
        attr_line_t()
×
187
            .append(" Notice: "_status_subtitle)
×
188
            .append(" No changes to undo")
×
189
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)),
×
190
    };
191

192
    return retval;
×
193
}
194

195
const std::vector<attr_line_t>&
196
textinput_curses::external_edit_failed()
×
197
{
198
    static const auto retval = std::vector{
199
        attr_line_t()
×
200
            .append(" Error: "_status_subtitle)
×
201
            .append(" Unable to write file for external edit")
×
202
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_ALERT_STATUS)),
×
203
    };
204

205
    return retval;
×
206
}
207

208
class textinput_mouse_delegate : public text_delegate {
209
public:
210
    textinput_mouse_delegate(textinput_curses* input) : tmd_input(input) {}
33✔
211

212
    bool text_handle_mouse(textview_curses& tc,
×
213
                           const listview_curses::display_line_content_t& dlc,
214
                           mouse_event& me) override
215
    {
216
        if (me.me_button == mouse_button_t::BUTTON_LEFT
×
217
            && me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED
×
218
            && dlc.is<listview_curses::main_content>())
×
219
        {
220
            ncinput ch{};
×
221

222
            ch.id = NCKEY_TAB;
×
223
            ch.eff_text[0] = '\t';
×
224
            ch.eff_text[1] = '\0';
×
225
            return this->tmd_input->handle_key(ch);
×
226
        }
227

228
        return false;
×
229
    }
230

231
    textinput_curses* tmd_input;
232
};
233

234
textinput_curses::textinput_curses()
33✔
235
{
236
    this->vc_enabled = false;
33✔
237
    this->vc_children.emplace_back(&this->tc_popup);
33✔
238

239
    this->tc_popup.tc_cursor_role = role_t::VCR_CURSOR_LINE;
33✔
240
    this->tc_popup.tc_disabled_cursor_role = role_t::VCR_DISABLED_CURSOR_LINE;
33✔
241
    this->tc_popup.lv_border_left_role = role_t::VCR_POPUP_BORDER;
33✔
242
    this->tc_popup.set_visible(false);
33✔
243
    this->tc_popup.set_title("textinput popup");
66✔
244
    this->tc_popup.set_head_space(0_vl);
33✔
245
    this->tc_popup.set_selectable(true);
33✔
246
    this->tc_popup.set_show_scrollbar(true);
33✔
247
    this->tc_popup.set_default_role(role_t::VCR_POPUP);
33✔
248
    this->tc_popup.set_sub_source(&this->tc_popup_source);
33✔
249
    this->tc_popup.set_delegate(
33✔
250
        std::make_shared<textinput_mouse_delegate>(this));
66✔
251

252
    this->vc_children.emplace_back(&this->tc_help_view);
33✔
253
    this->tc_help_view.set_visible(false);
33✔
254
    this->tc_help_view.set_title("textinput help");
66✔
255
    this->tc_help_view.set_show_scrollbar(true);
33✔
256
    this->tc_help_view.set_default_role(role_t::VCR_STATUS);
33✔
257
    this->tc_help_view.set_sub_source(&this->tc_help_source);
33✔
258

259
    this->tc_on_help = [](textinput_curses& ti) {
33✔
260
        ti.tc_mode = mode_t::show_help;
×
261
        ti.set_needs_update();
×
262
    };
33✔
263
    this->tc_help_source.replace_with(get_help_text());
33✔
264

265
    this->set_content("");
33✔
266
}
33✔
267

268
void
269
textinput_curses::content_to_lines(std::string content, int x)
37✔
270
{
271
    auto al = attr_line_t(content);
37✔
272

273
    if (!this->tc_prefix.empty()) {
37✔
274
        al.insert(0, this->tc_prefix);
×
275
        x += this->tc_prefix.length();
×
276
    }
277
    highlight_syntax(this->tc_text_format, al, x);
37✔
278
    if (!this->tc_prefix.empty()) {
37✔
279
        // XXX yuck
280
        al.erase(0, this->tc_prefix.al_string.size());
×
281
    }
282
    this->tc_doc_meta = lnav::document::discover(al)
37✔
283
                            .with_text_format(this->tc_text_format)
37✔
284
                            .save_words()
37✔
285
                            .perform();
37✔
286
    this->tc_lines = al.split_lines();
37✔
287
    if (endswith(al.al_string, "\n")) {
37✔
288
        this->tc_lines.emplace_back();
×
289
    }
290
    if (this->tc_lines.empty()) {
37✔
291
        this->tc_lines.emplace_back();
34✔
292
    } else {
293
        this->apply_highlights();
3✔
294
    }
295
}
37✔
296

297
void
298
textinput_curses::set_content(std::string content)
34✔
299
{
300
    this->content_to_lines(std::move(content), this->tc_prefix.length());
34✔
301

302
    this->tc_change_log.clear();
34✔
303
    this->tc_marks.clear();
34✔
304
    this->tc_notice = std::nullopt;
34✔
305
    this->tc_left = 0;
34✔
306
    this->tc_top = 0;
34✔
307
    this->tc_cursor = {};
34✔
308
    this->tc_abort_requested = false;
34✔
309
    this->clamp_point(this->tc_cursor);
34✔
310
    this->set_needs_update();
34✔
311
}
34✔
312

313
void
UNCOV
314
textinput_curses::set_height(int height)
×
315
{
316
    if (this->tc_height == height) {
×
UNCOV
317
        return;
×
318
    }
319

320
    this->tc_height = height;
×
321
    if (this->tc_height == 1) {
×
322
        if (this->tc_cursor.y != 0) {
×
UNCOV
323
            this->move_cursor_to(this->tc_cursor.copy_with_y(0));
×
324
        }
325
    }
UNCOV
326
    this->set_needs_update();
×
327
}
328

329
std::optional<view_curses*>
UNCOV
330
textinput_curses::contains(int x, int y)
×
331
{
332
    if (!this->vc_visible) {
×
UNCOV
333
        return std::nullopt;
×
334
    }
335

336
    auto child = view_curses::contains(x, y);
×
337
    if (child) {
×
UNCOV
338
        return child;
×
339
    }
340

341
    if (this->vc_x <= x && x < this->vc_x + this->vc_width && this->vc_y <= y
×
UNCOV
342
        && y < this->vc_y + this->tc_height)
×
343
    {
UNCOV
344
        return this;
×
345
    }
UNCOV
346
    return std::nullopt;
×
347
}
348

349
bool
UNCOV
350
textinput_curses::handle_mouse(mouse_event& me)
×
351
{
UNCOV
352
    ssize_t inner_height = this->tc_lines.size();
×
353

UNCOV
354
    log_debug("mouse here! button=%d state=%d x=%d y=%d",
×
355
              me.me_button,
356
              me.me_state,
357
              me.me_x,
358
              me.me_y);
359
    this->tc_notice = std::nullopt;
×
360
    this->tc_last_tick_after_input = std::nullopt;
×
361
    if (this->tc_mode == mode_t::show_help) {
×
UNCOV
362
        return this->tc_help_view.handle_mouse(me);
×
363
    }
364
    if (me.me_button == mouse_button_t::BUTTON_SCROLL_UP) {
×
365
        auto dim = this->get_visible_dimensions();
×
366
        if (this->tc_top > 0) {
×
367
            this->tc_top -= 1;
×
368
            if (this->tc_top + dim.dr_height - 2 < this->tc_cursor.y) {
×
UNCOV
369
                this->move_cursor_by({direction_t::up, 1});
×
370
            } else {
UNCOV
371
                this->ensure_cursor_visible();
×
372
            }
UNCOV
373
            this->set_needs_update();
×
374
        }
375
    } else if (me.me_button == mouse_button_t::BUTTON_SCROLL_DOWN) {
×
376
        auto dim = this->get_visible_dimensions();
×
377
        if (this->tc_top + dim.dr_height < inner_height) {
×
378
            this->tc_top += 1;
×
379
            if (this->tc_cursor.y <= this->tc_top) {
×
UNCOV
380
                this->move_cursor_by({direction_t::down, 1});
×
381
            } else {
UNCOV
382
                this->ensure_cursor_visible();
×
383
            }
UNCOV
384
            this->set_needs_update();
×
385
        }
386
    } else if (me.me_button == mouse_button_t::BUTTON_RIGHT) {
×
387
        if (this->tc_selection) {
×
388
            std::string content;
×
389
            auto range = this->tc_selection;
×
390
            auto add_nl = false;
×
391
            for (auto y = range->sr_start.y;
×
UNCOV
392
                 y <= range->sr_end.y && y < this->tc_lines.size();
×
393
                 ++y)
394
            {
395
                if (add_nl) {
×
UNCOV
396
                    content.push_back('\n');
×
397
                }
398
                auto sel_range = range->range_for_line(y);
×
399
                if (!sel_range) {
×
UNCOV
400
                    continue;
×
401
                }
402

403
                const auto& al = this->tc_lines[y];
×
404
                auto byte_start = al.column_to_byte_index(sel_range->lr_start);
×
405
                auto byte_end = al.column_to_byte_index(sel_range->lr_end);
×
406
                auto al_sf = string_fragment::from_str_range(
×
407
                    al.al_string, byte_start, byte_end);
×
408
                content += al_sf;
×
UNCOV
409
                add_nl = true;
×
410
            }
411

412
            this->tc_clipboard.clear();
×
413
            this->tc_cut_location = this->tc_cursor;
×
414
            this->tc_clipboard.emplace_back(content);
×
UNCOV
415
            this->sync_to_sysclip();
×
416
        }
417
    } else if (me.me_button == mouse_button_t::BUTTON_LEFT) {
×
418
        this->tc_mode = mode_t::editing;
×
419
        auto adj_press_x = me.me_press_x;
×
420
        if (me.me_press_y == 0 && me.me_press_x > 0) {
×
UNCOV
421
            adj_press_x -= this->tc_prefix.column_width();
×
422
        }
423
        auto adj_x = me.me_x;
×
424
        if (me.me_y == 0 && me.me_x > 0) {
×
UNCOV
425
            adj_x -= this->tc_prefix.column_width();
×
426
        }
427
        auto inner_press_point = input_point{
428
            this->tc_left + adj_press_x,
×
UNCOV
429
            (int) this->tc_top + me.me_press_y,
×
430
        };
UNCOV
431
        this->clamp_point(inner_press_point);
×
432
        auto inner_point = input_point{
433
            this->tc_left + adj_x,
×
UNCOV
434
            (int) this->tc_top + me.me_y,
×
435
        };
UNCOV
436
        this->clamp_point(inner_point);
×
437

438
        this->tc_popup_type = popup_type_t::none;
×
439
        this->tc_popup.set_visible(false);
×
440
        this->tc_complete_range = std::nullopt;
×
441
        this->tc_cursor = inner_point;
×
442
        log_debug("new cursor x=%d y=%d", this->tc_cursor.x, this->tc_cursor.y);
×
443
        if (me.me_state == mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK) {
×
444
            const auto& al = this->tc_lines[this->tc_cursor.y];
×
445
            auto sf = string_fragment::from_str(al.al_string);
×
446
            auto cursor_sf = sf.sub_cell_range(this->tc_left + adj_x,
×
447
                                               this->tc_left + adj_x);
×
UNCOV
448
            auto ds = data_scanner(sf);
×
449

450
            while (true) {
451
                auto tok_res = ds.tokenize2(this->tc_text_format);
×
452
                if (!tok_res.has_value()) {
×
UNCOV
453
                    break;
×
454
                }
455

456
                auto tok = tok_res.value();
×
UNCOV
457
                log_debug("tok %d", tok.tr_token);
×
458

459
                auto tok_sf = (tok.tr_token == data_token_t::DT_QUOTED_STRING
×
460
                               && (cursor_sf.sf_begin
×
461
                                       == tok.to_string_fragment().sf_begin
×
462
                                   || cursor_sf.sf_begin
×
463
                                       == tok.to_string_fragment().sf_end - 1))
×
464
                    ? tok.to_string_fragment()
×
465
                    : tok.inner_string_fragment();
×
UNCOV
466
                log_debug("tok %d:%d  curs %d:%d",
×
467
                          tok_sf.sf_begin,
468
                          tok_sf.sf_end,
469
                          cursor_sf.sf_begin,
470
                          cursor_sf.sf_end);
471
                if (tok_sf.contains(cursor_sf)
×
UNCOV
472
                    && tok.tr_token != data_token_t::DT_WHITE)
×
473
                {
UNCOV
474
                    log_debug("hit!");
×
475
                    auto group_tok
476
                        = ds.find_matching_bracket(this->tc_text_format, tok);
×
477
                    if (group_tok) {
×
UNCOV
478
                        tok_sf = group_tok.value().to_string_fragment();
×
479
                    }
480
                    auto tok_start = input_point{
481
                        (int) sf.byte_to_column_index(tok_sf.sf_begin)
×
482
                            - this->tc_left,
×
UNCOV
483
                        this->tc_cursor.y,
×
484
                    };
485
                    auto tok_end = input_point{
486
                        (int) sf.byte_to_column_index(tok_sf.sf_end)
×
487
                            - this->tc_left,
×
UNCOV
488
                        this->tc_cursor.y,
×
489
                    };
490

491
                    log_debug("st %d:%d", tok_start.x, tok_end.x);
×
UNCOV
492
                    this->tc_drag_selection = std::nullopt;
×
493
                    this->tc_selection
494
                        = selected_range::from_mouse(tok_start, tok_end);
×
UNCOV
495
                    this->set_needs_update();
×
496
                }
497
            }
498
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_PRESSED) {
×
499
            this->tc_selection = std::nullopt;
×
500
            this->tc_cursor_anchor = inner_press_point;
×
501
            this->tc_drag_selection = selected_range::from_mouse(
×
502
                this->tc_cursor_anchor, inner_point);
×
503
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED) {
×
504
            this->tc_drag_selection = selected_range::from_mouse(
×
505
                this->tc_cursor_anchor, inner_point);
×
506
            this->set_needs_update();
×
507
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
×
508
            this->tc_drag_selection = std::nullopt;
×
509
            if (inner_press_point == inner_point) {
×
UNCOV
510
                this->tc_selection = std::nullopt;
×
511
            } else {
512
                this->tc_selection = selected_range::from_mouse(
×
UNCOV
513
                    this->tc_cursor_anchor, inner_point);
×
514
            }
UNCOV
515
            this->set_needs_update();
×
516
        }
UNCOV
517
        this->ensure_cursor_visible();
×
518
    }
519

UNCOV
520
    return true;
×
521
}
522

523
bool
UNCOV
524
textinput_curses::handle_help_key(const ncinput& ch)
×
525
{
526
    switch (ch.id) {
×
UNCOV
527
        case ' ':
×
528
        case 'b':
529
        case 'j':
530
        case 'k':
531
        case 'g':
532
        case 'G':
533
        case NCKEY_HOME:
534
        case NCKEY_END:
535
        case NCKEY_UP:
536
        case NCKEY_DOWN:
537
        case NCKEY_PGUP:
538
        case NCKEY_PGDOWN: {
539
            log_debug("passing key press to help view");
×
UNCOV
540
            return this->tc_help_view.handle_key(ch);
×
541
        }
542
        default: {
×
543
            log_debug("switching back to editing from help");
×
544
            this->tc_mode = mode_t::editing;
×
545
            this->tc_help_view.set_visible(false);
×
546
            if (this->tc_on_change) {
×
UNCOV
547
                this->tc_on_change(*this);
×
548
            }
549
            this->set_needs_update();
×
UNCOV
550
            return true;
×
551
        }
552
    }
553
}
554

555
bool
UNCOV
556
textinput_curses::handle_search_key(const ncinput& ch)
×
557
{
558
    if (ncinput_ctrl_p(&ch)) {
×
559
        switch (ch.id) {
×
UNCOV
560
            case 'a':
×
561
            case 'A':
562
            case 'e':
563
            case 'E': {
564
                this->tc_mode = mode_t::editing;
×
UNCOV
565
                return this->handle_key(ch);
×
566
            }
UNCOV
567
            case 's':
×
568
            case 'S': {
569
                if (!this->tc_search.empty()) {
×
570
                    this->tc_search_start_point = this->tc_cursor;
×
UNCOV
571
                    this->move_cursor_to_next_search_hit();
×
572
                }
UNCOV
573
                return true;
×
574
            }
UNCOV
575
            case 'r':
×
576
            case 'R': {
577
                if (!this->tc_search.empty()) {
×
578
                    this->tc_search_start_point = this->tc_cursor;
×
UNCOV
579
                    this->move_cursor_to_prev_search_hit();
×
580
                }
UNCOV
581
                return true;
×
582
            }
583
        }
UNCOV
584
        return false;
×
585
    }
586

587
    switch (ch.id) {
×
588
        case NCKEY_ESC:
×
589
            this->tc_mode = mode_t::editing;
×
590
            this->set_needs_update();
×
591
            return true;
×
592
        case NCKEY_BACKSPACE: {
×
593
            if (!this->tc_search.empty()) {
×
594
                if (this->tc_search_found.has_value()) {
×
UNCOV
595
                    this->tc_search.pop_back();
×
596
                    auto compile_res = lnav::pcre2pp::code::from(
597
                        lnav::pcre2pp::quote(this->tc_search), PCRE2_CASELESS);
×
598
                    this->tc_search_code = compile_res.unwrap().to_shared();
×
599
                } else {
×
600
                    this->tc_search.clear();
×
UNCOV
601
                    this->tc_search_code.reset();
×
602
                }
UNCOV
603
                this->move_cursor_to_next_search_hit();
×
604
            }
UNCOV
605
            return true;
×
606
        }
607
        case NCKEY_ENTER: {
×
608
            this->tc_search_start_point = this->tc_cursor;
×
609
            this->move_cursor_to_next_search_hit();
×
UNCOV
610
            return true;
×
611
        }
UNCOV
612
        case NCKEY_LEFT:
×
613
        case NCKEY_RIGHT:
614
        case NCKEY_UP:
615
        case NCKEY_DOWN: {
616
            this->tc_mode = mode_t::editing;
×
617
            this->handle_key(ch);
×
UNCOV
618
            return true;
×
619
        }
UNCOV
620
        default: {
×
621
            char utf8[32];
622
            size_t index = 0;
×
623
            for (const auto eff_ch : ch.eff_text) {
×
624
                if (eff_ch == 0) {
×
UNCOV
625
                    break;
×
626
                }
627
                ww898::utf::utf8::write(eff_ch,
×
628
                                        [&utf8, &index](const char bits) {
×
629
                                            utf8[index] = bits;
×
630
                                            index += 1;
×
UNCOV
631
                                        });
×
632
            }
633
            if (index > 0) {
×
UNCOV
634
                utf8[index] = 0;
×
635

636
                if (!this->tc_search_found.has_value()) {
×
UNCOV
637
                    this->tc_search.clear();
×
638
                }
639
                this->tc_search.append(utf8);
×
UNCOV
640
                if (!this->tc_search.empty()) {
×
641
                    auto compile_res = lnav::pcre2pp::code::from(
642
                        lnav::pcre2pp::quote(this->tc_search), PCRE2_CASELESS);
×
643
                    this->tc_search_code = compile_res.unwrap().to_shared();
×
UNCOV
644
                    this->move_cursor_to_next_search_hit();
×
645
                }
646
            }
UNCOV
647
            return true;
×
648
        }
649
    }
650

651
    return false;
652
}
653

654
void
UNCOV
655
textinput_curses::move_cursor_to_next_search_hit()
×
656
{
657
    if (this->tc_search_code == nullptr) {
×
UNCOV
658
        return;
×
659
    }
660

661
    auto x = this->tc_search_start_point.x;
×
662
    if (this->tc_search_found && !this->tc_search_found.value()) {
×
UNCOV
663
        this->tc_search_start_point.y = 0;
×
664
    }
665
    this->tc_search_found = false;
×
666
    for (auto y = this->tc_search_start_point.y;
×
UNCOV
667
         y < (ssize_t) this->tc_lines.size();
×
668
         y++)
669
    {
UNCOV
670
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
×
671

672
        const auto& al = this->tc_lines[y];
×
673
        auto byte_x = al.column_to_byte_index(x);
×
674
        auto after_x_sf = al.to_string_fragment().substr(byte_x);
×
675
        auto find_res = this->tc_search_code->capture_from(after_x_sf)
×
676
                            .into(md)
×
677
                            .matches()
×
678
                            .ignore_error();
×
UNCOV
679
        if (find_res) {
×
680
            this->tc_cursor.x
681
                = al.byte_to_column_index(find_res.value().f_all.sf_end);
×
682
            this->tc_cursor.y = y;
×
UNCOV
683
            log_debug(
×
684
                "search found %d:%d", this->tc_cursor.x, this->tc_cursor.y);
685
            this->tc_search_found = true;
×
686
            this->ensure_cursor_visible();
×
UNCOV
687
            break;
×
688
        }
UNCOV
689
        x = 0;
×
690
    }
UNCOV
691
    this->set_needs_update();
×
692
}
693

694
void
UNCOV
695
textinput_curses::move_cursor_to_prev_search_hit()
×
696
{
697
    auto max_x = std::make_optional(this->tc_search_start_point.x);
×
698
    if (this->tc_search_found && !this->tc_search_found.value()) {
×
UNCOV
699
        this->tc_search_start_point.y = this->tc_lines.size() - 1;
×
700
    }
701
    this->tc_search_found = false;
×
702
    for (auto y = this->tc_search_start_point.y; y >= 0; y--) {
×
UNCOV
703
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
×
704

705
        const auto& al = this->tc_lines[y];
×
706
        auto before_x_sf = al.to_string_fragment();
×
707
        if (max_x) {
×
UNCOV
708
            before_x_sf = before_x_sf.sub_cell_range(0, max_x.value());
×
709
        }
710
        auto find_res = this->tc_search_code->capture_from(before_x_sf)
×
711
                            .into(md)
×
712
                            .matches()
×
713
                            .ignore_error();
×
UNCOV
714
        if (find_res) {
×
715
            auto new_input_point = input_point{
UNCOV
716
                (int) al.byte_to_column_index(find_res.value().f_all.sf_end),
×
717
                y,
718
            };
719
            if (new_input_point != this->tc_cursor) {
×
720
                this->tc_cursor = new_input_point;
×
721
                this->tc_search_found = true;
×
722
                this->ensure_cursor_visible();
×
UNCOV
723
                break;
×
724
            }
725
        }
UNCOV
726
        max_x = std::nullopt;
×
727
    }
UNCOV
728
    this->set_needs_update();
×
729
}
730

731
void
UNCOV
732
textinput_curses::command_indent(indent_mode_t mode)
×
733
{
UNCOV
734
    log_debug("indenting line: %d", this->tc_cursor.y);
×
735

736
    if (this->tc_cursor.y == 0 && !this->tc_prefix.empty()) {
×
UNCOV
737
        return;
×
738
    }
739

740
    int indent_amount = 0;
×
741
    switch (mode) {
×
UNCOV
742
        case indent_mode_t::left:
×
743
        case indent_mode_t::clear_left:
744
            indent_amount = 0;
×
745
            break;
×
746
        case indent_mode_t::right:
×
747
            indent_amount = 4;
×
UNCOV
748
            break;
×
749
    }
750
    auto& al = this->tc_lines[this->tc_cursor.y];
×
751
    auto line_sf = al.to_string_fragment();
×
752
    const auto [before, after]
×
753
        = line_sf.split_when([](auto ch) { return !isspace(ch); });
×
UNCOV
754
    auto indent_iter = std::lower_bound(this->tc_doc_meta.m_indents.begin(),
×
755
                                        this->tc_doc_meta.m_indents.end(),
756
                                        before.length());
×
757
    if (indent_iter != this->tc_doc_meta.m_indents.end()) {
×
758
        if (mode == indent_mode_t::left || mode == indent_mode_t::clear_left) {
×
759
            if (indent_iter == this->tc_doc_meta.m_indents.begin()) {
×
UNCOV
760
                indent_amount = 0;
×
761
            } else {
UNCOV
762
                indent_amount = *std::prev(indent_iter);
×
763
            }
764
        } else if (before.empty()) {
×
UNCOV
765
            indent_amount = *indent_iter;
×
766
        } else {
767
            auto next_indent_iter = std::next(indent_iter);
×
768
            if (next_indent_iter == this->tc_doc_meta.m_indents.end()) {
×
UNCOV
769
                indent_amount += *indent_iter;
×
770
            } else {
UNCOV
771
                indent_amount = *next_indent_iter;
×
772
            }
773
        }
774
    }
775
    auto sel_len = (before.empty() && mode == indent_mode_t::clear_left)
×
776
        ? line_sf.column_width()
×
777
        : before.length();
×
778
    this->tc_selection = selected_range::from_key(
×
779
        this->tc_cursor.copy_with_x(0), this->tc_cursor.copy_with_x(sel_len));
×
780
    auto indent = std::string(indent_amount, ' ');
×
781
    auto old_cursor = this->tc_cursor;
×
782
    this->replace_selection(indent);
×
UNCOV
783
    this->tc_cursor.x = indent.length() - sel_len + old_cursor.x;
×
784
}
785

786
void
UNCOV
787
textinput_curses::command_down(const ncinput& ch)
×
788
{
789
    if (this->tc_popup.is_visible()) {
×
790
        this->tc_popup.handle_key(ch);
×
791
        if (this->tc_on_popup_change) {
×
792
            this->tc_in_popup_change = true;
×
793
            this->tc_on_popup_change(*this);
×
UNCOV
794
            this->tc_in_popup_change = false;
×
795
        }
796
    } else {
797
        ssize_t inner_height = this->tc_lines.size();
×
798
        if (ncinput_shift_p(&ch)) {
×
799
            if (!this->tc_selection) {
×
UNCOV
800
                this->tc_cursor_anchor = this->tc_cursor;
×
801
            }
802
        }
803
        if (this->tc_cursor.y + 1 < inner_height) {
×
UNCOV
804
            this->move_cursor_by({direction_t::down, 1});
×
805
        } else {
806
            this->move_cursor_to({
×
807
                (int) this->tc_lines[this->tc_cursor.y].column_width(),
×
UNCOV
808
                (int) this->tc_lines.size() - 1,
×
809
            });
810
        }
811
        if (ncinput_shift_p(&ch)) {
×
812
            this->tc_selection = selected_range::from_key(
×
UNCOV
813
                this->tc_cursor_anchor, this->tc_cursor);
×
814
        }
815
    }
816
}
817

818
void
UNCOV
819
textinput_curses::command_up(const ncinput& ch)
×
820
{
821
    if (this->tc_popup.is_visible()) {
×
822
        this->tc_popup.handle_key(ch);
×
823
        if (this->tc_on_popup_change) {
×
824
            this->tc_in_popup_change = true;
×
825
            this->tc_on_popup_change(*this);
×
UNCOV
826
            this->tc_in_popup_change = false;
×
827
        }
828
    } else if (this->tc_height == 1) {
×
829
        if (this->tc_on_history_list) {
×
UNCOV
830
            this->tc_on_history_list(*this);
×
831
        }
832
    } else {
833
        if (ncinput_shift_p(&ch)) {
×
834
            log_debug("up shift");
×
835
            if (!this->tc_selection) {
×
UNCOV
836
                this->tc_cursor_anchor = this->tc_cursor;
×
837
            }
838
        }
839
        if (this->tc_cursor.y > 0) {
×
UNCOV
840
            this->move_cursor_by({direction_t::up, 1});
×
841
        } else {
UNCOV
842
            this->move_cursor_to({0, 0});
×
843
        }
844
        if (ncinput_shift_p(&ch)) {
×
845
            this->tc_selection = selected_range::from_key(
×
UNCOV
846
                this->tc_cursor_anchor, this->tc_cursor);
×
847
        }
848
    }
849
}
850

851
bool
852
textinput_curses::handle_key(const ncinput& ch)
4✔
853
{
854
    static const auto PREFIX_RE = lnav::pcre2pp::code::from_const(
855
        R"(^\s*((?:-|\*|1\.|>)(?:\s+\[( |x|X)\])?\s*))");
4✔
856
    static const auto PREFIX_OR_WS_RE = lnav::pcre2pp::code::from_const(
857
        R"(^\s*(>\s*|(?:-|\*|1\.)?(?:\s+\[( |x|X)\])?\s+))");
4✔
858
    thread_local auto md = lnav::pcre2pp::match_data::unitialized();
4✔
859

860
    if (this->tc_abort_requested && ch.id != NCKEY_ESC) {
4✔
861
        this->tc_abort_requested = false;
×
862
        this->set_needs_update();
×
863
    }
864
    if (this->tc_notice) {
4✔
UNCOV
865
        this->tc_notice = std::nullopt;
×
UNCOV
866
        switch (ch.id) {
×
867
            case NCKEY_F01:
×
868
            case NCKEY_UP:
869
            case NCKEY_DOWN:
870
            case NCKEY_LEFT:
871
            case NCKEY_RIGHT:
UNCOV
872
                break;
×
UNCOV
873
            default:
×
874
                return true;
×
875
        }
876
    }
877
    this->tc_last_tick_after_input = std::nullopt;
4✔
878
    switch (this->tc_mode) {
4✔
UNCOV
879
        case mode_t::searching:
×
UNCOV
880
            return this->handle_search_key(ch);
×
UNCOV
881
        case mode_t::show_help:
×
UNCOV
882
            return this->handle_help_key(ch);
×
883
        case mode_t::editing:
4✔
884
            break;
4✔
885
    }
886

887
    if (this->tc_mode == mode_t::searching) {
4✔
UNCOV
888
        return this->handle_search_key(ch);
×
889
    }
890

891
    auto dim = this->get_visible_dimensions();
4✔
892
    auto inner_height = this->tc_lines.size();
4✔
893
    auto bottom = inner_height - 1;
4✔
894
    auto chid = ch.id;
4✔
895

896
    if (ch.id == NCKEY_PASTE) {
4✔
897
        static const auto lf_re = lnav::pcre2pp::code::from_const("\r\n?");
898
        auto paste_sf = string_fragment::from_c_str(ch.paste_content);
×
899
        if (!this->tc_selection) {
×
900
            this->tc_selection = selected_range::from_point(this->tc_cursor);
×
901
        }
UNCOV
902
        auto text = lf_re.replace(paste_sf, "\n");
×
UNCOV
903
        log_debug("applying bracketed paste of size %zu", text.length());
×
904
        this->replace_selection(text);
×
905
        return true;
×
906
    }
907

908
    if (ncinput_alt_p(&ch)) {
4✔
UNCOV
909
        switch (chid) {
×
910
            case NCKEY_LEFT: {
×
911
                auto& al = this->tc_lines[this->tc_cursor.y];
×
912
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
UNCOV
913
                                        .prev_word(this->tc_cursor.x);
×
914

915
                this->move_cursor_to(
×
916
                    this->tc_cursor.copy_with_x(next_col_opt.value_or(0)));
×
917
                return true;
×
918
            }
UNCOV
919
            case NCKEY_RIGHT: {
×
920
                auto& al = this->tc_lines[this->tc_cursor.y];
×
921
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
UNCOV
922
                                        .next_word(this->tc_cursor.x);
×
UNCOV
923
                this->move_cursor_to(
×
924
                    this->tc_cursor.copy_with_x(next_col_opt.value_or(
UNCOV
925
                        this->tc_lines[this->tc_cursor.y].column_width())));
×
UNCOV
926
                return true;
×
927
            }
928
        }
929
    }
930

931
    if (ncinput_ctrl_p(&ch)) {
4✔
UNCOV
932
        if (ncinput_shift_p(&ch) && chid == '-') {
×
933
            chid = '_';  // XXX
×
934
        }
UNCOV
935
        switch (chid) {
×
936
            case 'a':
×
937
            case 'A': {
938
                this->move_cursor_to(this->tc_cursor.copy_with_x(0));
×
939
                return true;
×
940
            }
941
            case 'b':
×
942
            case 'B': {
943
                chid = NCKEY_LEFT;
×
944
                break;
×
945
            }
UNCOV
946
            case 'e':
×
947
            case 'E': {
UNCOV
948
                this->move_cursor_to(this->tc_cursor.copy_with_x(
×
949
                    this->tc_lines[this->tc_cursor.y].column_width()));
×
950
                return true;
×
951
            }
952
            case 'f':
×
953
            case 'F': {
954
                chid = NCKEY_RIGHT;
×
955
                break;
×
956
            }
UNCOV
957
            case 'k':
×
958
            case 'K': {
UNCOV
959
                if (this->tc_selection) {
×
UNCOV
960
                    auto range = this->tc_selection;
×
961
                    log_debug("cutting selection [%d:%d) - [%d:%d)",
×
962
                              range->sr_start.x,
963
                              range->sr_start.y,
964
                              range->sr_end.x,
965
                              range->sr_end.y);
966
                    auto new_clip = std::string();
×
967
                    for (auto curr_line = range->sr_start.y;
×
968
                         curr_line <= range->sr_end.y;
×
969
                         ++curr_line)
970
                    {
971
                        auto sel_range = range->range_for_line(curr_line);
×
UNCOV
972
                        if (!sel_range) {
×
973
                            continue;
×
974
                        }
975

UNCOV
976
                        auto& al = this->tc_lines[curr_line];
×
977
                        auto start_byte
978
                            = al.column_to_byte_index(sel_range->lr_start);
×
979
                        auto end_byte
UNCOV
980
                            = al.column_to_byte_index(sel_range->lr_end);
×
981
                        auto sub
UNCOV
982
                            = al.subline(start_byte, end_byte - start_byte);
×
983
                        if (curr_line > range->sr_start.y) {
×
984
                            new_clip.push_back('\n');
×
985
                        }
986
                        new_clip.append(sub.al_string);
×
987
                    }
UNCOV
988
                    this->tc_clipboard.clear();
×
UNCOV
989
                    this->tc_clipboard.emplace_back(new_clip);
×
990
                    this->replace_selection(string_fragment{});
×
991
                } else {
×
992
                    log_debug("cutting from %d to end of line %d",
×
993
                              this->tc_cursor.x,
994
                              this->tc_cursor.y);
UNCOV
995
                    if (this->tc_cursor != this->tc_cut_location) {
×
996
                        log_debug("  cursor moved, clearing clipboard");
×
997
                        this->tc_clipboard.clear();
×
998
                    }
999
                    auto& al = this->tc_lines[this->tc_cursor.y];
×
1000
                    auto byte_index
1001
                        = al.column_to_byte_index(this->tc_cursor.x);
×
1002
                    this->tc_clipboard.emplace_back(
×
1003
                        al.subline(byte_index).al_string);
×
1004
                    this->tc_selection = selected_range::from_key(
×
1005
                        this->tc_cursor,
1006
                        this->tc_cursor.copy_with_x(al.column_width()));
×
1007
                    if (this->tc_selection->empty()
×
UNCOV
1008
                        && this->tc_cursor.y + 1
×
1009
                            < (ssize_t) this->tc_lines.size())
×
1010
                    {
1011
                        this->tc_clipboard.back().push_back('\n');
×
1012
                        this->tc_selection = selected_range::from_key(
×
1013
                            this->tc_cursor,
1014
                            input_point{0, this->tc_cursor.y + 1});
×
1015
                    }
1016
                    this->replace_selection(string_fragment{});
×
1017
                    this->tc_cut_location = this->tc_cursor;
×
1018
                }
1019
                this->sync_to_sysclip();
×
UNCOV
1020
                this->tc_drag_selection = std::nullopt;
×
1021
                this->update_lines();
×
1022
                return true;
×
1023
            }
UNCOV
1024
            case 'l':
×
1025
            case 'L': {
UNCOV
1026
                log_debug("reformat content");
×
1027
                if (this->tc_on_reformat) {
×
UNCOV
1028
                    this->tc_on_reformat(*this);
×
1029
                }
1030
                return true;
×
1031
            }
1032
            case 'n':
×
1033
            case 'N': {
1034
                chid = NCKEY_DOWN;
×
1035
                break;
×
1036
            }
UNCOV
1037
            case 'o':
×
1038
            case 'O': {
UNCOV
1039
                log_debug("opening in external editor");
×
1040
                if (this->tc_on_external_open) {
×
UNCOV
1041
                    this->tc_on_external_open(*this);
×
1042
                }
1043
                return true;
×
1044
            }
1045
            case 'p':
×
1046
            case 'P': {
1047
                chid = NCKEY_UP;
×
1048
                break;
×
1049
            }
1050
            case 'r':
×
1051
            case 'R': {
1052
                if (this->tc_on_history_search) {
×
UNCOV
1053
                    this->tc_on_history_search(*this);
×
1054
                }
1055
                return true;
×
1056
            }
1057
            case 's':
×
1058
            case 'S': {
1059
                if (this->tc_height > 1) {
×
UNCOV
1060
                    log_debug("switching to search mode from edit");
×
1061
                    this->tc_mode = mode_t::searching;
×
UNCOV
1062
                    this->tc_search_start_point = this->tc_cursor;
×
1063
                    this->tc_search_found = std::nullopt;
×
UNCOV
1064
                    this->set_needs_update();
×
1065
                }
1066
                return true;
×
1067
            }
1068
            case 'u':
×
1069
            case 'U': {
1070
                log_debug("cutting to beginning of line");
×
UNCOV
1071
                auto& al = this->tc_lines[this->tc_cursor.y];
×
1072
                auto byte_index = al.column_to_byte_index(this->tc_cursor.x);
×
1073
                if (this->tc_cursor != this->tc_cut_location) {
×
1074
                    log_debug("  cursor moved, clearing clipboard");
×
1075
                    this->tc_clipboard.clear();
×
1076
                }
1077
                this->tc_clipboard.emplace_back(
×
1078
                    al.subline(0, byte_index).al_string);
×
1079
                this->sync_to_sysclip();
×
1080
                this->tc_selection = selected_range::from_key(
×
1081
                    this->tc_cursor.copy_with_x(0), this->tc_cursor);
×
1082
                this->replace_selection(string_fragment{});
×
UNCOV
1083
                this->tc_cut_location = this->tc_cursor;
×
1084
                this->tc_selection = std::nullopt;
×
UNCOV
1085
                this->tc_drag_selection = std::nullopt;
×
1086
                this->update_lines();
×
UNCOV
1087
                return true;
×
1088
            }
1089
            case 'w':
×
1090
            case 'W': {
1091
                log_debug("cutting to beginning of previous word");
×
1092
                auto al_sf
1093
                    = this->tc_lines[this->tc_cursor.y].to_string_fragment();
×
1094
                auto prev_word_start_opt = al_sf.prev_word(this->tc_cursor.x);
×
1095
                if (!prev_word_start_opt && this->tc_cursor.x > 0) {
×
UNCOV
1096
                    prev_word_start_opt = 0;
×
1097
                }
1098
                if (prev_word_start_opt) {
×
UNCOV
1099
                    if (this->tc_cut_location != this->tc_cursor) {
×
1100
                        log_debug(
×
1101
                            "  cursor moved since last cut, clearing "
1102
                            "clipboard");
1103
                        this->tc_clipboard.clear();
×
1104
                    }
UNCOV
1105
                    auto prev_word = al_sf.sub_cell_range(
×
1106
                        prev_word_start_opt.value(), this->tc_cursor.x);
×
1107
                    this->tc_clipboard.emplace_front(prev_word.to_string());
×
1108
                    this->sync_to_sysclip();
×
1109
                    this->tc_selection = selected_range::from_key(
×
1110
                        this->tc_cursor.copy_with_x(
1111
                            prev_word_start_opt.value()),
×
UNCOV
1112
                        this->tc_cursor);
×
1113
                    this->replace_selection(string_fragment{});
×
UNCOV
1114
                    this->tc_cut_location = this->tc_cursor;
×
1115
                }
1116
                return true;
×
1117
            }
1118
            case 'x':
×
1119
            case 'X': {
1120
                log_debug("performing action");
×
UNCOV
1121
                this->blur();
×
1122
                if (this->tc_on_perform) {
×
UNCOV
1123
                    this->tc_on_perform(*this);
×
1124
                }
1125
                return true;
×
1126
            }
1127
            case 'y':
×
1128
            case 'Y': {
1129
                log_debug("pasting clipboard contents");
×
UNCOV
1130
                for (const auto& clipping : this->tc_clipboard) {
×
UNCOV
1131
                    auto& al = this->tc_lines[this->tc_cursor.y];
×
1132
                    al.insert(al.column_to_byte_index(this->tc_cursor.x),
×
1133
                              clipping);
1134
                    const auto clip_sf = string_fragment::from_str(clipping);
×
1135
                    const auto clip_cols
1136
                        = clip_sf
1137
                              .find_left_boundary(clip_sf.length(),
×
UNCOV
1138
                                                  string_fragment::tag1{'\n'})
×
1139
                              .column_width();
×
UNCOV
1140
                    auto line_count = clip_sf.count('\n');
×
1141
                    if (line_count > 0) {
×
1142
                        this->tc_cursor.x = 0;
×
1143
                    } else {
1144
                        this->tc_cursor.x += clip_cols;
×
1145
                    }
1146
                    this->tc_cursor.y += line_count;
×
UNCOV
1147
                    this->tc_selection = std::nullopt;
×
1148
                    this->tc_drag_selection = std::nullopt;
×
1149
                    this->update_lines();
×
1150
                }
1151
                return true;
×
1152
            }
1153
            case ']': {
×
UNCOV
1154
                if (this->tc_popup.is_visible()) {
×
1155
                    this->tc_popup_type = popup_type_t::none;
×
UNCOV
1156
                    this->tc_popup.set_visible(false);
×
UNCOV
1157
                    this->tc_complete_range = std::nullopt;
×
1158
                    this->set_needs_update();
×
1159
                } else {
1160
                    this->abort();
×
1161
                }
1162

1163
                this->tc_selection = std::nullopt;
×
1164
                this->tc_drag_selection = std::nullopt;
×
1165
                return true;
×
1166
            }
1167
            case '_': {
×
1168
                if (this->tc_change_log.empty()) {
×
1169
                    this->tc_notice = no_changes();
×
1170
                    this->set_needs_update();
×
1171
                } else {
UNCOV
1172
                    log_debug("undo!");
×
UNCOV
1173
                    const auto& ce = this->tc_change_log.back();
×
UNCOV
1174
                    auto content_sf = string_fragment::from_str(ce.ce_content);
×
UNCOV
1175
                    this->tc_selection = ce.ce_range;
×
UNCOV
1176
                    log_debug(" range [%d:%d) - [%d:%d) - %s",
×
1177
                              this->tc_selection->sr_start.x,
1178
                              this->tc_selection->sr_start.y,
1179
                              this->tc_selection->sr_end.x,
1180
                              this->tc_selection->sr_end.y,
1181
                              ce.ce_content.c_str());
1182
                    this->replace_selection_no_change(content_sf);
×
1183
                    this->tc_change_log.pop_back();
×
1184
                }
1185
                return true;
×
1186
            }
UNCOV
1187
            default: {
×
UNCOV
1188
                this->tc_notice = unhandled_input();
×
UNCOV
1189
                this->set_needs_update();
×
UNCOV
1190
                return false;
×
1191
            }
1192
        }
1193
    }
1194

1195
    switch (chid) {
4✔
UNCOV
1196
        case NCKEY_ESC:
×
1197
        case KEY_CTRL(']'): {
1198
            if (this->tc_popup.is_visible()) {
×
1199
                if (this->tc_on_popup_cancel) {
×
1200
                    this->tc_on_popup_cancel(*this);
×
1201
                }
1202
                this->tc_popup_type = popup_type_t::none;
×
UNCOV
1203
                this->tc_popup.set_visible(false);
×
UNCOV
1204
                this->tc_complete_range = std::nullopt;
×
1205
                this->set_needs_update();
×
1206
            } else if (chid != NCKEY_ESC || this->tc_abort_requested) {
×
1207
                this->abort();
×
1208
            } else {
UNCOV
1209
                this->tc_abort_requested = true;
×
UNCOV
1210
                this->set_needs_update();
×
1211
            }
1212

1213
            this->tc_selection = std::nullopt;
×
UNCOV
1214
            this->tc_drag_selection = std::nullopt;
×
1215
            return true;
×
1216
        }
1217
        case NCKEY_ENTER: {
1✔
1218
            if (this->tc_popup.is_visible()) {
1✔
UNCOV
1219
                this->tc_popup.set_visible(false);
×
UNCOV
1220
                if (this->tc_on_completion) {
×
UNCOV
1221
                    this->tc_on_completion(*this);
×
1222
                }
1223
                this->tc_popup_type = popup_type_t::none;
×
1224
                this->set_needs_update();
×
1225
            } else if (this->tc_height == 1) {
1✔
1226
                this->blur();
1✔
1227
                if (this->tc_on_perform) {
1✔
1228
                    this->tc_on_perform(*this);
1✔
1229
                }
1230
            } else {
1231
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
1232
                auto al_sf = al.to_string_fragment();
×
1233
                auto prefix_sf = al_sf.rtrim(" ");
×
1234
                auto indent = std::string("\n");
×
1235
                if (!this->tc_selection) {
×
UNCOV
1236
                    log_debug("checking for prefix");
×
1237
                    auto match_opt = PREFIX_OR_WS_RE.capture_from(al_sf)
×
1238
                                         .into(md)
×
UNCOV
1239
                                         .matches()
×
1240
                                         .ignore_error();
×
1241
                    if (match_opt) {
×
1242
                        log_debug("has prefix");
×
1243
                        this->tc_selection = selected_range::from_key(
×
1244
                            this->tc_cursor.copy_with_x(
1245
                                prefix_sf.column_width()),
×
1246
                            this->tc_cursor.copy_with_x(al_sf.column_width()));
×
1247
                        auto is_comment
1248
                            = al.al_attrs
×
1249
                            | lnav::itertools::find_if(
×
UNCOV
1250
                                  [](const string_attr& sa) {
×
1251
                                      return (sa.sa_type == &VC_ROLE)
×
1252
                                          && sa.sa_value.get<role_t>()
×
1253
                                          == role_t::VCR_COMMENT;
×
1254
                                  });
×
1255
                        if (!is_comment && !al.empty()
×
1256
                            && !md[1]->startswith(">")
×
UNCOV
1257
                            && match_opt->f_all.length() == al.length())
×
1258
                        {
1259
                            log_debug("clear left");
×
UNCOV
1260
                            this->command_indent(indent_mode_t::clear_left);
×
1261
                        } else if (this->is_cursor_at_end_of_line()) {
×
UNCOV
1262
                            indent += match_opt->f_all;
×
UNCOV
1263
                            if (md[2] && md[2]->front() != ' ') {
×
UNCOV
1264
                                indent[1 + md[2]->sf_begin] = ' ';
×
1265
                            }
1266
                        } else {
UNCOV
1267
                            indent.append(match_opt->f_all.length(), ' ');
×
1268
                            this->tc_selection
UNCOV
1269
                                = selected_range::from_point(this->tc_cursor);
×
1270
                        }
1271
                    } else {
1272
                        this->tc_selection
UNCOV
1273
                            = selected_range::from_point(this->tc_cursor);
×
UNCOV
1274
                        log_debug("no prefix, replace point: [%d:%d]",
×
1275
                                  this->tc_selection->sr_start.x,
1276
                                  this->tc_selection->sr_start.y);
1277
                    }
1278
                }
1279
                this->replace_selection(indent);
×
1280
            }
1281
            // TODO implement "double enter" to call tc_on_perform
1282
            return true;
1✔
1283
        }
1284
        case NCKEY_TAB: {
×
1285
            if (this->tc_popup.is_visible()) {
×
1286
                log_debug("performing completion");
×
UNCOV
1287
                this->tc_popup_type = popup_type_t::none;
×
1288
                this->tc_popup.set_visible(false);
×
1289
                if (this->tc_on_completion) {
×
1290
                    this->tc_on_completion(*this);
×
1291
                }
1292
                this->set_needs_update();
×
1293
            } else if (!this->tc_suggestion.empty()
×
1294
                       && this->is_cursor_at_end_of_line())
×
1295
            {
UNCOV
1296
                log_debug("inserting suggestion");
×
1297
                this->tc_selection = selected_range::from_key(this->tc_cursor,
×
1298
                                                              this->tc_cursor);
×
1299
                this->replace_selection(this->tc_suggestion);
×
1300
            } else if (this->tc_height == 1) {
×
UNCOV
1301
                log_debug("requesting completion at %d", this->tc_cursor.x);
×
UNCOV
1302
                if (this->tc_on_completion_request) {
×
1303
                    this->tc_on_completion_request(*this);
×
1304
                }
1305
            } else if (!this->tc_selection) {
×
UNCOV
1306
                if (!ncinput_shift_p(&ch)
×
1307
                    && (this->tc_cursor.x > 0
×
1308
                        && this->tc_lines[this->tc_cursor.y].al_string.back()
×
1309
                            != ' '))
1310
                {
UNCOV
1311
                    log_debug("requesting completion at %d", this->tc_cursor.x);
×
UNCOV
1312
                    if (this->tc_on_completion_request) {
×
1313
                        this->tc_on_completion_request(*this);
×
1314
                    }
UNCOV
1315
                    if (!this->tc_popup.is_visible()) {
×
UNCOV
1316
                        this->command_indent(indent_mode_t::right);
×
1317
                    }
UNCOV
1318
                    return true;
×
1319
                }
1320

1321
                this->command_indent(ncinput_shift_p(&ch)
×
1322
                                         ? indent_mode_t::left
1323
                                         : indent_mode_t::right);
1324
            }
1325
            return true;
×
1326
        }
1327
        case NCKEY_HOME: {
×
1328
            this->move_cursor_to(input_point::home());
×
1329
            return true;
×
1330
        }
1331
        case NCKEY_END: {
×
UNCOV
1332
            this->move_cursor_to(input_point::end());
×
1333
            return true;
×
1334
        }
1335
        case NCKEY_PGUP: {
×
1336
            if (this->tc_cursor.y > 0) {
×
UNCOV
1337
                this->move_cursor_by({direction_t::up, (size_t) dim.dr_height});
×
1338
            }
UNCOV
1339
            return true;
×
1340
        }
1341
        case NCKEY_PGDOWN: {
×
UNCOV
1342
            if (this->tc_cursor.y < (ssize_t) bottom) {
×
1343
                this->move_cursor_by(
×
1344
                    {direction_t::down, (size_t) dim.dr_height});
×
1345
            }
UNCOV
1346
            return true;
×
1347
        }
1348
        case NCKEY_DEL: {
×
1349
            this->tc_selection = selected_range::from_key(
×
1350
                this->tc_cursor,
1351
                this->tc_cursor + movement{direction_t::right, 1});
×
1352
            this->replace_selection(string_fragment{});
×
1353
            break;
×
1354
        }
1355
        case NCKEY_BACKSPACE: {
×
1356
            if (this->tc_lines.size() == 1 && this->tc_lines.front().empty()) {
×
1357
                this->abort();
×
1358
            } else if (!this->tc_selection) {
×
1359
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
1360
                auto line_sf = al.to_string_fragment();
×
1361
                const auto [before, after]
×
1362
                    = line_sf
1363
                          .split_n(
×
1364
                              line_sf.column_to_byte_index(this->tc_cursor.x))
×
UNCOV
1365
                          .value();
×
1366
                auto match_opt = PREFIX_RE.capture_from(before)
×
1367
                                     .into(md)
×
1368
                                     .matches()
×
1369
                                     .ignore_error();
×
1370

1371
                if (match_opt && !match_opt->f_all.empty()
×
1372
                    && match_opt->f_all.sf_end == this->tc_cursor.x)
×
1373
                {
1374
                    auto is_comment = al.al_attrs
×
1375
                        | lnav::itertools::find_if([](const string_attr& sa) {
×
UNCOV
1376
                                          return (sa.sa_type == &VC_ROLE)
×
1377
                                              && sa.sa_value.get<role_t>()
×
UNCOV
1378
                                              == role_t::VCR_COMMENT;
×
1379
                                      });
×
1380
                    if (!is_comment && md[1]) {
×
UNCOV
1381
                        this->tc_selection = selected_range::from_key(
×
UNCOV
1382
                            this->tc_cursor.copy_with_x(md[1]->sf_begin),
×
UNCOV
1383
                            this->tc_cursor);
×
1384
                        auto indent = std::string(
UNCOV
1385
                            md[1]->startswith(">") ? 0 : md[1]->length(), ' ');
×
1386

1387
                        this->replace_selection(indent);
×
1388
                        return true;
×
1389
                    }
1390
                } else {
1391
                    auto indent_iter
1392
                        = std::lower_bound(this->tc_doc_meta.m_indents.begin(),
×
1393
                                           this->tc_doc_meta.m_indents.end(),
UNCOV
1394
                                           this->tc_cursor.x);
×
UNCOV
1395
                    if (indent_iter != this->tc_doc_meta.m_indents.end()) {
×
UNCOV
1396
                        if (indent_iter != this->tc_doc_meta.m_indents.begin())
×
1397
                        {
UNCOV
1398
                            auto prev_indent_iter = std::prev(indent_iter);
×
1399
                            this->tc_selection = selected_range::from_key(
×
1400
                                this->tc_cursor.copy_with_x(*prev_indent_iter),
×
UNCOV
1401
                                this->tc_cursor);
×
1402
                        }
1403
                    }
1404
                }
UNCOV
1405
                if (!this->tc_selection) {
×
1406
                    this->tc_selection
1407
                        = selected_range::from_point_and_movement(
×
1408
                            this->tc_cursor, movement{direction_t::left, 1});
×
1409
                }
1410
            }
1411
            this->replace_selection(string_fragment{});
×
1412
            return true;
×
1413
        }
1414
        case NCKEY_UP: {
×
1415
            this->command_up(ch);
×
1416
            return true;
×
1417
        }
UNCOV
1418
        case NCKEY_DOWN: {
×
1419
            this->command_down(ch);
×
1420
            return true;
×
1421
        }
1422
        case NCKEY_LEFT: {
×
1423
            if (ncinput_shift_p(&ch)) {
×
1424
                if (!this->tc_selection) {
×
1425
                    this->tc_cursor_anchor = this->tc_cursor;
×
1426
                }
1427
                this->move_cursor_by({direction_t::left, 1});
×
UNCOV
1428
                this->tc_selection = selected_range::from_key(
×
1429
                    this->tc_cursor_anchor, this->tc_cursor);
×
UNCOV
1430
            } else if (this->tc_selection) {
×
1431
                this->tc_cursor = this->tc_selection->sr_start;
×
1432
                this->tc_selection = std::nullopt;
×
1433
                this->set_needs_update();
×
1434
            } else {
UNCOV
1435
                this->move_cursor_by({direction_t::left, 1});
×
1436
            }
1437
            return true;
×
1438
        }
1439
        case NCKEY_RIGHT: {
×
1440
            if (ncinput_shift_p(&ch)) {
×
1441
                if (!this->tc_selection) {
×
1442
                    this->tc_cursor_anchor = this->tc_cursor;
×
1443
                }
1444
                this->move_cursor_by({direction_t::right, 1});
×
UNCOV
1445
                this->tc_selection = selected_range::from_key(
×
1446
                    this->tc_cursor_anchor, this->tc_cursor);
×
UNCOV
1447
            } else if (this->tc_selection) {
×
1448
                this->tc_cursor = this->tc_selection->sr_end;
×
1449
                this->tc_selection = std::nullopt;
×
1450
                this->set_needs_update();
×
1451
            } else {
1452
                this->move_cursor_by({direction_t::right, 1});
×
1453
            }
1454
            return true;
×
1455
        }
1456
        case NCKEY_F01: {
×
1457
            if (this->tc_on_help) {
×
1458
                this->tc_on_help(*this);
×
1459
            }
UNCOV
1460
            return true;
×
1461
        }
UNCOV
1462
        case ' ': {
×
1463
            if (!this->tc_selection) {
×
UNCOV
1464
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
1465
                const auto sf = al.to_string_fragment();
×
1466
                if (PREFIX_RE.capture_from(sf).into(md).found_p() && md[2]
×
1467
                    && this->tc_cursor.x == md[2]->sf_begin)
×
1468
                {
UNCOV
1469
                    this->tc_selection = selected_range::from_key(
×
1470
                        this->tc_cursor,
1471
                        this->tc_cursor.copy_with_x(this->tc_cursor.x + 1));
×
1472

1473
                    auto repl = (md[2]->front() == ' ') ? "X"_frag : " "_frag;
×
1474
                    this->replace_selection(repl);
×
UNCOV
1475
                    return true;
×
1476
                }
1477

1478
                this->tc_selection
1479
                    = selected_range::from_point(this->tc_cursor);
×
1480
            }
UNCOV
1481
            this->replace_selection(" "_frag);
×
UNCOV
1482
            return true;
×
1483
        }
1484
        default: {
3✔
1485
            if (NCKEY_F00 <= ch.id && ch.id <= NCKEY_F60) {
3✔
UNCOV
1486
                this->tc_notice = unhandled_input();
×
UNCOV
1487
                this->set_needs_update();
×
1488
            } else {
1489
                char utf8[32];
1490
                size_t index = 0;
3✔
1491
                for (const auto eff_ch : ch.eff_text) {
6✔
1492
                    log_debug(" eff %x", eff_ch);
6✔
1493
                    if (eff_ch == 0) {
6✔
1494
                        break;
3✔
1495
                    }
1496
                    ww898::utf::utf8::write(eff_ch,
3✔
1497
                                            [&utf8, &index](const char bits) {
3✔
1498
                                                utf8[index] = bits;
3✔
1499
                                                index += 1;
3✔
1500
                                            });
3✔
1501
                }
1502
                if (index > 0) {
3✔
1503
                    utf8[index] = 0;
3✔
1504

1505
                    if (!this->tc_selection) {
3✔
1506
                        this->tc_selection
1507
                            = selected_range::from_point(this->tc_cursor);
3✔
1508
                    }
1509
                    this->replace_selection(string_fragment::from_c_str(utf8));
3✔
1510
                } else {
1511
                    this->tc_notice = unhandled_input();
×
UNCOV
1512
                    this->set_needs_update();
×
1513
                }
1514
            }
1515
            return true;
3✔
1516
        }
1517
    }
1518

UNCOV
1519
    return false;
×
1520
}
1521

1522
void
1523
textinput_curses::ensure_cursor_visible()
17✔
1524
{
1525
    if (!this->vc_enabled) {
17✔
1526
        return;
14✔
1527
    }
1528

1529
    auto dim = this->get_visible_dimensions();
3✔
1530
    auto orig_top = this->tc_top;
3✔
1531
    auto orig_left = this->tc_left;
3✔
1532
    auto orig_cursor = this->tc_cursor;
3✔
1533
    auto orig_max_cursor_x = this->tc_max_cursor_x;
3✔
1534

1535
    this->clamp_point(this->tc_cursor);
3✔
1536
    if (this->tc_cursor.y < 0) {
3✔
UNCOV
1537
        this->tc_cursor.y = 0;
×
1538
    }
1539
    if (this->tc_cursor.y >= (ssize_t) this->tc_lines.size()) {
3✔
UNCOV
1540
        this->tc_cursor.y = this->tc_lines.size() - 1;
×
1541
    }
1542
    if (this->tc_cursor.x < 0) {
3✔
UNCOV
1543
        this->tc_cursor.x = 0;
×
1544
    }
1545
    if (this->tc_cursor.x
6✔
1546
        >= (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1547
    {
1548
        this->tc_cursor.x = this->tc_lines[this->tc_cursor.y].column_width();
3✔
1549
    }
1550

1551
    if (this->tc_cursor.x <= this->tc_left) {
3✔
UNCOV
1552
        this->tc_left = this->tc_cursor.x;
×
1553
        if (this->tc_left > 0) {
×
UNCOV
1554
            this->tc_left -= 1;
×
1555
        }
1556
    }
1557
    if (this->tc_cursor.x >= this->tc_left + (dim.dr_width - 2)) {
3✔
1558
        this->tc_left = (this->tc_cursor.x - dim.dr_width) + 2;
×
1559
    }
1560
    if (this->tc_top < 0) {
3✔
UNCOV
1561
        this->tc_top = 0;
×
1562
    }
1563
    if (this->tc_top >= this->tc_cursor.y) {
3✔
1564
        this->tc_top = this->tc_cursor.y;
3✔
1565
        if (this->tc_top > 0) {
3✔
UNCOV
1566
            this->tc_top -= 1;
×
1567
        }
1568
    }
1569
    if (this->tc_height > 1
3✔
1570
        && this->tc_cursor.y + 1 >= this->tc_top + dim.dr_height)
×
1571
    {
UNCOV
1572
        this->tc_top = (this->tc_cursor.y + 1 - dim.dr_height) + 1;
×
1573
    }
1574
    if (this->tc_top + dim.dr_height > (ssize_t) this->tc_lines.size()) {
3✔
UNCOV
1575
        if ((ssize_t) this->tc_lines.size() > dim.dr_height) {
×
UNCOV
1576
            this->tc_top = this->tc_lines.size() - dim.dr_height + 1;
×
1577
        } else {
1578
            this->tc_top = 0;
×
1579
        }
1580
    }
1581
    if (!this->tc_in_popup_change && this->tc_popup.is_visible()
3✔
UNCOV
1582
        && this->tc_complete_range
×
1583
        && !this->tc_complete_range->contains(this->tc_cursor))
6✔
1584
    {
UNCOV
1585
        this->tc_popup.set_visible(false);
×
UNCOV
1586
        this->tc_complete_range = std::nullopt;
×
1587
    }
1588

1589
    if (this->tc_cursor.x
6✔
1590
        == (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1591
    {
1592
        if (this->tc_cursor.x >= this->tc_max_cursor_x) {
3✔
1593
            this->tc_max_cursor_x = this->tc_cursor.x;
3✔
1594
        }
1595
    } else {
UNCOV
1596
        this->tc_max_cursor_x = this->tc_cursor.x;
×
1597
    }
1598

1599
    if (orig_top != this->tc_top || orig_left != this->tc_left
3✔
1600
        || orig_cursor != this->tc_cursor
3✔
1601
        || orig_max_cursor_x != this->tc_max_cursor_x)
6✔
1602
    {
1603
        this->set_needs_update();
3✔
1604
    }
1605
}
1606

1607
void
1608
textinput_curses::apply_highlights()
3✔
1609
{
1610
    if (this->tc_text_format == text_format_t::TF_LNAV_SCRIPT) {
3✔
1611
        return;
×
1612
    }
1613

1614
    for (auto& line : this->tc_lines) {
6✔
1615
        for (const auto& hl_pair : this->tc_highlights) {
3✔
UNCOV
1616
            const auto& hl = hl_pair.second;
×
1617

UNCOV
1618
            if (!hl.applies_to_format(this->tc_text_format)) {
×
UNCOV
1619
                continue;
×
1620
            }
UNCOV
1621
            hl.annotate(line, line_range{0, -1});
×
1622
        }
1623
    }
1624
}
1625

1626
std::string
1627
textinput_curses::replace_selection_no_change(string_fragment sf)
3✔
1628
{
1629
    if (!this->tc_selection) {
3✔
UNCOV
1630
        return "";
×
1631
    }
1632

1633
    std::optional<int> del_max;
3✔
1634
    auto full_first_line = false;
3✔
1635
    std::string retval;
3✔
1636

1637
    auto range = std::exchange(this->tc_selection, std::nullopt).value();
3✔
1638
    this->tc_cursor.y = range.sr_start.y;
3✔
1639
    for (auto curr_line = range.sr_start.y;
6✔
1640
         curr_line <= range.sr_end.y && curr_line < this->tc_lines.size();
6✔
1641
         ++curr_line)
1642
    {
1643
        auto sel_range = range.range_for_line(curr_line);
3✔
1644

1645
        if (!sel_range) {
3✔
1646
            continue;
×
1647
        }
1648

1649
        log_debug("sel_range y=%d [%d:%d)",
3✔
1650
                  curr_line,
1651
                  sel_range->lr_start,
1652
                  sel_range->lr_end);
1653
        if (sel_range->lr_start < 0) {
3✔
1654
            if (curr_line > 0) {
×
UNCOV
1655
                log_debug("append %d to %d", curr_line, curr_line - 1);
×
1656
                this->tc_cursor.x
UNCOV
1657
                    = this->tc_lines[curr_line - 1].column_width();
×
UNCOV
1658
                this->tc_cursor.y = curr_line - 1;
×
UNCOV
1659
                this->tc_lines[curr_line - 1].append(this->tc_lines[curr_line]);
×
UNCOV
1660
                retval.push_back('\n');
×
UNCOV
1661
                del_max = curr_line;
×
1662
                full_first_line = true;
×
1663
            }
1664
        } else if (sel_range->lr_start
3✔
1665
                       == (ssize_t) this->tc_lines[curr_line].column_width()
3✔
1666
                   && sel_range->lr_end != -1
3✔
1667
                   && sel_range->lr_start < sel_range->lr_end)
6✔
1668
        {
1669
            // Del deleting line feed
1670
            if (curr_line + 1 < (ssize_t) this->tc_lines.size()) {
×
1671
                this->tc_lines[curr_line].append(this->tc_lines[curr_line + 1]);
×
1672
                retval.push_back('\n');
×
1673
                del_max = curr_line + 1;
×
1674
            }
1675
        } else if (sel_range->lr_start == 0 && sel_range->lr_end == -1) {
3✔
UNCOV
1676
            log_debug("delete full line");
×
UNCOV
1677
            retval.append(this->tc_lines[curr_line].al_string);
×
UNCOV
1678
            retval.push_back('\n');
×
UNCOV
1679
            del_max = curr_line;
×
UNCOV
1680
            if (curr_line == range.sr_start.y) {
×
UNCOV
1681
                log_debug("full first");
×
UNCOV
1682
                full_first_line = true;
×
1683
            }
1684
        } else {
1685
            log_debug("partial line change");
3✔
1686
            auto& al = this->tc_lines[curr_line];
3✔
1687
            auto start = al.column_to_byte_index(sel_range->lr_start);
3✔
1688
            auto end = sel_range->lr_end == -1
3✔
1689
                ? al.al_string.length()
3✔
1690
                : al.column_to_byte_index(sel_range->lr_end);
3✔
1691

1692
            retval.append(al.al_string.substr(start, end - start));
3✔
1693
            if (sel_range->lr_end == -1) {
3✔
UNCOV
1694
                retval.push_back('\n');
×
1695
            }
1696
            al.erase(start, end - start);
3✔
1697
            if (full_first_line || curr_line == range.sr_start.y) {
3✔
1698
                al.insert(start, sf.to_string());
3✔
1699
                this->tc_cursor.x = sel_range->lr_start;
3✔
1700
            }
1701
            if (!full_first_line && sel_range->lr_start == 0
3✔
1702
                && range.sr_start.y < curr_line && curr_line == range.sr_end.y)
6✔
1703
            {
UNCOV
1704
                del_max = curr_line;
×
UNCOV
1705
                this->tc_lines[range.sr_start.y].append(al);
×
1706
            }
1707
        }
1708
    }
1709

1710
    if (del_max) {
3✔
UNCOV
1711
        log_debug("deleting lines [%d+%d:%d)",
×
1712
                  range.sr_start.y,
1713
                  (full_first_line ? 0 : 1),
1714
                  del_max.value() + 1);
UNCOV
1715
        this->tc_lines.erase(this->tc_lines.begin() + range.sr_start.y
×
UNCOV
1716
                                 + (full_first_line ? 0 : 1),
×
UNCOV
1717
                             this->tc_lines.begin() + del_max.value() + 1);
×
1718
    }
1719

1720
    const auto repl_last_line
1721
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'});
3✔
1722
    log_debug(
3✔
1723
        "last line '%.*s'", repl_last_line.length(), repl_last_line.data());
1724
    const auto repl_cols
1725
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'})
3✔
1726
              .column_width();
3✔
1727
    const auto repl_lines = sf.count('\n');
3✔
1728
    log_debug("repl_cols => %zu", repl_cols);
3✔
1729
    if (repl_lines > 0) {
3✔
1730
        this->tc_cursor.x = repl_cols;
×
1731
    } else {
1732
        this->tc_cursor.x += repl_cols;
3✔
1733
    }
1734
    this->tc_cursor.y += repl_lines;
3✔
1735

1736
    this->tc_drag_selection = std::nullopt;
3✔
1737
    if (retval == sf) {
3✔
UNCOV
1738
        if (!sf.empty()) {
×
UNCOV
1739
            this->content_to_lines(this->get_content(),
×
1740
                                   this->get_cursor_offset());
1741
        }
1742
    } else {
1743
        this->update_lines();
3✔
1744
    }
1745

1746
    ensure(!this->tc_lines.empty());
3✔
1747

1748
    return retval;
3✔
1749
}
3✔
1750

1751
void
1752
textinput_curses::replace_selection(string_fragment sf)
3✔
1753
{
1754
    static constexpr uint32_t mask
1755
        = UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N | UC_CATEGORY_MASK_Pc;
1756

1757
    if (!this->tc_selection) {
3✔
UNCOV
1758
        return;
×
1759
    }
1760
    auto range = this->tc_selection.value();
3✔
1761
    auto change_pos = this->tc_change_log.size();
3✔
1762
    auto old_text = this->replace_selection_no_change(sf);
3✔
1763
    if (old_text == sf) {
3✔
UNCOV
1764
        log_trace("no-op replacement");
×
1765
    } else {
1766
        auto is_wordbreak = !sf.empty()
3✔
1767
            && !uc_is_general_category_withtable(sf.front_codepoint(), mask);
3✔
1768
        log_debug("repl sel [%d:%d) - cursor [%d:%d)",
3✔
1769
                  range.sr_start.x,
1770
                  range.sr_start.y,
1771
                  this->tc_cursor.x,
1772
                  this->tc_cursor.y);
1773
        if (this->tc_change_log.empty()
3✔
1774
            || this->tc_change_log.back().ce_range.sr_end != range.sr_start
2✔
1775
            || is_wordbreak)
5✔
1776
        {
1777
            auto redo_range = selected_range::from_key(
1✔
1778
                range.sr_start.x < 0 ? this->tc_cursor : range.sr_start,
1✔
1779
                this->tc_cursor);
1780
            log_debug("  redo range [%d:%d] - [%d:%d]",
1✔
1781
                      redo_range.sr_start.x,
1782
                      redo_range.sr_start.y,
1783
                      redo_range.sr_end.x,
1784
                      redo_range.sr_end.y);
1785
            if (change_pos < this->tc_change_log.size()) {
1✔
1786
                // XXX an on_change handler can run and do its own replacement
1787
                // before we get a change to add or entry
UNCOV
1788
                log_debug("inserting change log at %zu", change_pos);
×
UNCOV
1789
                this->tc_change_log.insert(
×
UNCOV
1790
                    std::next(this->tc_change_log.begin(), change_pos),
×
UNCOV
1791
                    change_entry{redo_range, old_text});
×
1792
            } else {
1793
                this->tc_change_log.emplace_back(redo_range, old_text);
1✔
1794
            }
1795
        } else {
1796
            auto& last_range = this->tc_change_log.back().ce_range;
2✔
1797
            last_range.sr_end = this->tc_cursor;
2✔
1798
            log_debug("extending undo range [%d:%d] - [%d:%d]",
2✔
1799
                      last_range.sr_start.x,
1800
                      last_range.sr_start.y,
1801
                      last_range.sr_end.x,
1802
                      last_range.sr_end.y);
1803
        }
1804
    }
1805
}
3✔
1806

1807
void
UNCOV
1808
textinput_curses::move_cursor_by(movement move)
×
1809
{
UNCOV
1810
    auto cursor_y_offset = this->tc_cursor.y - this->tc_top;
×
1811
    this->tc_cursor += move;
×
1812
    if (move.hm_dir == direction_t::up || move.hm_dir == direction_t::down) {
×
1813
        if (move.hm_amount > 1) {
×
UNCOV
1814
            this->tc_top = this->tc_cursor.y - cursor_y_offset;
×
1815
            this->set_needs_update();
×
1816
        }
1817
        this->tc_cursor.x = this->tc_max_cursor_x;
×
1818
    }
UNCOV
1819
    if (this->tc_cursor.x < 0) {
×
1820
        if (this->tc_cursor.y > 0) {
×
1821
            this->tc_cursor.y -= 1;
×
1822
            this->tc_cursor.x
UNCOV
1823
                = this->tc_lines[this->tc_cursor.y].column_width();
×
1824
        } else {
1825
            this->tc_cursor.x = 0;
×
1826
        }
1827
    }
UNCOV
1828
    if (move.hm_dir == direction_t::right
×
UNCOV
1829
        && this->tc_cursor.x
×
1830
            > (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
×
1831
    {
1832
        if (this->tc_cursor.y + 1 < (ssize_t) this->tc_lines.size()) {
×
1833
            this->tc_cursor.x = 0;
×
UNCOV
1834
            this->tc_cursor.y += 1;
×
1835
            this->tc_max_cursor_x = 0;
×
1836
        }
1837
    }
UNCOV
1838
    this->clamp_point(this->tc_cursor);
×
1839
    if (this->tc_drag_selection) {
×
UNCOV
1840
        this->tc_drag_selection = std::nullopt;
×
UNCOV
1841
        this->set_needs_update();
×
1842
    }
1843
    if (this->tc_selection) {
×
UNCOV
1844
        this->tc_selection = std::nullopt;
×
1845
        this->set_needs_update();
×
1846
    }
1847
    this->ensure_cursor_visible();
×
1848
}
1849

1850
void
1851
textinput_curses::move_cursor_to(input_point ip)
×
1852
{
UNCOV
1853
    this->tc_cursor = ip;
×
1854
    if (this->tc_drag_selection) {
×
UNCOV
1855
        this->tc_drag_selection = std::nullopt;
×
UNCOV
1856
        this->set_needs_update();
×
1857
    }
UNCOV
1858
    if (this->tc_selection) {
×
UNCOV
1859
        this->tc_selection = std::nullopt;
×
UNCOV
1860
        this->set_needs_update();
×
1861
    }
UNCOV
1862
    this->ensure_cursor_visible();
×
1863
}
1864

1865
void
1866
textinput_curses::update_lines()
3✔
1867
{
1868
    const auto x = this->get_cursor_offset();
3✔
1869
    this->content_to_lines(this->get_content(), x);
3✔
1870
    this->set_needs_update();
3✔
1871
    this->ensure_cursor_visible();
3✔
1872

1873
    this->tc_marks.clear();
3✔
1874
    if (this->tc_in_popup_change) {
3✔
UNCOV
1875
        log_trace("in popup change, skipping");
×
1876
    } else {
1877
        this->tc_popup.set_visible(false);
3✔
1878
        this->tc_complete_range = std::nullopt;
3✔
1879
        if (this->tc_on_change) {
3✔
1880
            this->tc_on_change(*this);
3✔
1881
        }
1882
        if (!this->tc_popup.is_visible()) {
3✔
1883
            this->tc_popup_type = popup_type_t::none;
3✔
1884
        }
1885
    }
1886

1887
    ensure(!this->tc_lines.empty());
3✔
1888
}
3✔
1889

1890
textinput_curses::dimension_result
1891
textinput_curses::get_visible_dimensions() const
23✔
1892
{
1893
    dimension_result retval;
23✔
1894

1895
    ncplane_dim_yx(
23✔
1896
        this->tc_window, &retval.dr_full_height, &retval.dr_full_width);
23✔
1897

1898
    if (this->vc_y < (ssize_t) retval.dr_full_height) {
23✔
1899
        retval.dr_height = std::min((int) retval.dr_full_height - this->vc_y,
23✔
1900
                                    this->tc_height);
23✔
1901
    }
1902
    if (this->vc_x < (ssize_t) retval.dr_full_width) {
23✔
1903
        retval.dr_width = std::min((long) retval.dr_full_width - this->vc_x,
23✔
1904
                                   this->vc_width);
23✔
1905
    }
1906
    return retval;
23✔
1907
}
1908

1909
std::string
1910
textinput_curses::get_content(bool trim) const
7✔
1911
{
1912
    auto need_lf = false;
7✔
1913
    std::string retval;
7✔
1914

1915
    for (const auto& al : this->tc_lines) {
14✔
1916
        const auto& line = al.al_string;
7✔
1917
        auto line_sf = string_fragment::from_str(line);
7✔
1918
        if (trim) {
7✔
UNCOV
1919
            line_sf = line_sf.rtrim(" ");
×
1920
        }
1921
        if (need_lf) {
7✔
UNCOV
1922
            retval.push_back('\n');
×
1923
        }
1924
        retval += line_sf;
7✔
1925
        need_lf = true;
7✔
1926
    }
1927
    return retval;
7✔
1928
}
×
1929

1930
void
1931
textinput_curses::focus()
2✔
1932
{
1933
    if (!this->vc_enabled) {
2✔
1934
        this->vc_enabled = true;
1✔
1935
        if (this->tc_on_focus) {
1✔
UNCOV
1936
            this->tc_on_focus(*this);
×
1937
        }
1938
        this->set_needs_update();
1✔
1939
    }
1940

1941
    if (this->tc_mode == mode_t::show_help
4✔
1942
        || (this->tc_height && this->tc_notice)
2✔
1943
        || (this->tc_selection
4✔
1944
            && this->tc_selection->contains_exclusive(this->tc_cursor)))
2✔
1945
    {
UNCOV
1946
        notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
×
UNCOV
1947
        return;
×
1948
    }
1949
    auto term_x = this->vc_x + this->tc_cursor.x - this->tc_left;
2✔
1950
    if (this->tc_cursor.y == 0) {
2✔
1951
        term_x += this->tc_prefix.column_width();
2✔
1952
    }
1953
    notcurses_cursor_enable(ncplane_notcurses(this->tc_window),
2✔
1954
                            this->vc_y + this->tc_cursor.y - this->tc_top,
2✔
1955
                            term_x);
1956
}
1957

1958
void
1959
textinput_curses::blur()
1✔
1960
{
1961
    this->tc_popup_type = popup_type_t::none;
1✔
1962
    this->tc_popup.set_visible(false);
1✔
1963
    this->vc_enabled = false;
1✔
1964
    if (this->tc_on_blur) {
1✔
1965
        this->tc_on_blur(*this);
1✔
1966
    }
1967

1968
    notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
1✔
1969
    this->set_needs_update();
1✔
1970
}
1✔
1971

1972
void
UNCOV
1973
textinput_curses::abort()
×
1974
{
UNCOV
1975
    this->blur();
×
1976
    this->tc_selection = std::nullopt;
×
UNCOV
1977
    this->tc_drag_selection = std::nullopt;
×
1978
    if (this->tc_on_abort) {
×
UNCOV
1979
        this->tc_on_abort(*this);
×
1980
    }
1981
}
1982

1983
void
1984
textinput_curses::sync_to_sysclip() const
×
1985
{
1986
    auto clip_open_res = sysclip::open(sysclip::type_t::GENERAL);
×
1987

UNCOV
1988
    if (clip_open_res.isOk()) {
×
UNCOV
1989
        auto clip_file = clip_open_res.unwrap();
×
UNCOV
1990
        fmt::print(clip_file.in(),
×
UNCOV
1991
                   FMT_STRING("{}"),
×
UNCOV
1992
                   fmt::join(this->tc_clipboard, ""));
×
UNCOV
1993
    } else {
×
UNCOV
1994
        auto err_msg = clip_open_res.unwrapErr();
×
UNCOV
1995
        log_error("unable to open clipboard: %s", err_msg.c_str());
×
1996
    }
1997
}
1998

1999
bool
2000
textinput_curses::do_update()
24✔
2001
{
2002
    static auto& vc = view_colors::singleton();
24✔
2003
    auto retval = false;
24✔
2004

2005
    if (!this->is_visible()) {
24✔
2006
        return retval;
1✔
2007
    }
2008

2009
    auto popup_height = this->tc_popup.get_height();
23✔
2010
    auto rel_y = (this->tc_popup_type == popup_type_t::history
23✔
2011
                      ? 0
23✔
2012
                      : this->tc_cursor.y - this->tc_top)
23✔
2013
        - popup_height;
23✔
2014
    if (this->vc_y + rel_y < 0) {
23✔
UNCOV
2015
        rel_y = this->tc_cursor.y - this->tc_top + popup_height + 1;
×
2016
    }
2017
    this->tc_popup.set_y(this->vc_y + rel_y);
23✔
2018

2019
    if (!this->vc_needs_update) {
23✔
2020
        return view_curses::do_update();
7✔
2021
    }
2022

2023
    auto dim = this->get_visible_dimensions();
16✔
2024
    if (!this->vc_enabled) {
16✔
2025
        ncplane_erase_region(
15✔
2026
            this->tc_window, this->vc_y, this->vc_x, 1, dim.dr_width);
2027
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
15✔
UNCOV
2028
        mvwattrline(this->tc_window,
×
2029
                    this->vc_y,
2030
                    this->vc_x,
2031
                    this->tc_inactive_value,
15✔
2032
                    lr);
2033

2034
        if (!this->tc_alt_value.empty()
15✔
2035
            && (ssize_t) (this->tc_inactive_value.column_width() + 3
19✔
2036
                          + this->tc_alt_value.column_width())
4✔
2037
                < dim.dr_width)
4✔
2038
        {
2039
            auto alt_x = dim.dr_width - this->tc_alt_value.column_width();
4✔
2040
            auto lr = line_range{0, (int) this->tc_alt_value.column_width()};
4✔
2041
            mvwattrline(
4✔
2042
                this->tc_window, this->vc_y, alt_x, this->tc_alt_value, lr);
4✔
2043
        }
2044

2045
        this->vc_needs_update = false;
15✔
2046
        return true;
15✔
2047
    }
2048

2049
    if (this->tc_mode == mode_t::show_help) {
1✔
UNCOV
2050
        this->tc_help_view.set_window(this->tc_window);
×
UNCOV
2051
        this->tc_help_view.set_x(this->vc_x);
×
UNCOV
2052
        this->tc_help_view.set_y(this->vc_y);
×
UNCOV
2053
        this->tc_help_view.set_width(this->vc_width);
×
UNCOV
2054
        this->tc_help_view.set_height(vis_line_t(this->tc_height));
×
UNCOV
2055
        this->tc_help_view.set_visible(true);
×
UNCOV
2056
        return view_curses::do_update();
×
2057
    }
2058

2059
    retval = true;
1✔
2060
    ssize_t row_count = this->tc_lines.size();
1✔
2061
    auto y = this->vc_y;
1✔
2062
    auto y_max = this->vc_y + dim.dr_height;
1✔
2063
    if (row_count == 1 && this->tc_lines[0].empty()
1✔
2064
        && !this->tc_suggestion.empty())
2✔
2065
    {
UNCOV
2066
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
UNCOV
2067
        auto al = attr_line_t(this->tc_suggestion)
×
UNCOV
2068
                      .with_attr_for_all(VC_ROLE.value(role_t::VCR_SUGGESTION));
×
UNCOV
2069
        al.insert(0, this->tc_prefix);
×
UNCOV
2070
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
×
UNCOV
2071
        mvwattrline(this->tc_window, y, this->vc_x, al, lr);
×
UNCOV
2072
        row_count -= 1;
×
UNCOV
2073
        y += 1;
×
2074
    }
2075
    auto abort_msg_shown = false;
1✔
2076
    for (auto curr_line = this->tc_top; curr_line < row_count && y < y_max;
2✔
2077
         curr_line++, y++)
1✔
2078
    {
2079
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
1✔
2080
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
1✔
2081
        auto al = this->tc_lines[curr_line];
1✔
2082
        if (this->tc_drag_selection) {
1✔
2083
            auto sel_lr = this->tc_drag_selection->range_for_line(curr_line);
×
UNCOV
2084
            if (sel_lr) {
×
UNCOV
2085
                al.al_attrs.emplace_back(
×
UNCOV
2086
                    sel_lr.value(), VC_ROLE.value(role_t::VCR_SELECTED_TEXT));
×
2087
            }
2088
        } else if (this->tc_selection) {
1✔
2089
            auto sel_lr = this->tc_selection->range_for_line(curr_line);
×
2090
            if (sel_lr) {
×
2091
                al.al_attrs.emplace_back(
×
2092
                    sel_lr.value(), VC_STYLE.value(text_attrs::with_reverse()));
×
2093
            }
2094
        }
2095
        if (this->tc_mode == mode_t::searching
2✔
2096
            && this->tc_search_found.value_or(false))
1✔
2097
        {
UNCOV
2098
            this->tc_search_code->capture_from(al.al_string)
×
UNCOV
2099
                .for_each([&al](lnav::pcre2pp::match_data& md) {
×
2100
                    al.al_attrs.emplace_back(
×
UNCOV
2101
                        line_range{
×
UNCOV
2102
                            md[0]->sf_begin,
×
2103
                            md[0]->sf_end,
×
2104
                        },
UNCOV
2105
                        VC_ROLE.value(role_t::VCR_SEARCH));
×
UNCOV
2106
                });
×
2107
        }
2108
        if (!this->tc_suggestion.empty() && !this->tc_popup.is_visible()
1✔
UNCOV
2109
            && curr_line == this->tc_cursor.y
×
2110
            && this->tc_cursor.x == (ssize_t) al.column_width())
1✔
2111
        {
UNCOV
2112
            al.append(this->tc_suggestion,
×
UNCOV
2113
                      VC_ROLE.value(role_t::VCR_SUGGESTION));
×
2114
        }
2115
        if (curr_line == 0) {
1✔
2116
            al.insert(0, this->tc_prefix);
1✔
2117
        }
2118
        mvwattrline(this->tc_window, y, this->vc_x, al, lr);
1✔
2119

2120
        if (!abort_msg_shown && this->tc_abort_requested) {
1✔
2121
            static auto REQ_MSG = attr_line_t("  Press ")
2122
                                      .append("Esc"_hotkey)
×
UNCOV
2123
                                      .append(" to abort  ")
×
2124
                                      .with_attr_for_all(VC_ROLE.value(
×
2125
                                          role_t::VCR_STATUS));
×
2126
            auto msg_lr = line_range{0, 0 + dim.dr_width};
×
2127
            mvwattrline(
×
2128
                this->tc_window,
2129
                y,
UNCOV
2130
                this->vc_x + dim.dr_width - REQ_MSG.utf8_length_or_length(),
×
2131
                REQ_MSG,
2132
                msg_lr);
2133
            abort_msg_shown = true;
×
2134
        }
2135
    }
1✔
2136
    for (; y < y_max; y++) {
1✔
2137
        static constexpr auto EMPTY_LR = line_range::empty_at(0);
2138

UNCOV
2139
        auto al = attr_line_t();
×
2140
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
2141
        mvwattrline(
×
2142
            this->tc_window, y, this->vc_x, al, EMPTY_LR, role_t::VCR_ALT_ROW);
2143
    }
2144
    if (this->tc_notice) {
1✔
2145
        auto notice_lines = this->tc_notice.value();
×
2146
        auto avail_height = std::min(dim.dr_height, (int) notice_lines.size());
×
UNCOV
2147
        auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2148

2149
        for (auto& al : notice_lines) {
×
2150
            auto lr = line_range{0, dim.dr_width};
×
UNCOV
2151
            mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
UNCOV
2152
            if (notice_y >= y_max) {
×
UNCOV
2153
                break;
×
2154
            }
2155
        }
2156
    } else if (this->tc_mode == mode_t::searching) {
1✔
2157
        auto search_prompt = attr_line_t(" ");
×
2158
        if (this->tc_search.empty() || this->tc_search_found.has_value()) {
×
UNCOV
2159
            search_prompt.append(this->tc_search)
×
2160
                .append(" ", VC_ROLE.value(role_t::VCR_CURSOR_LINE));
×
2161
        } else {
2162
            search_prompt.append(this->tc_search,
×
2163
                                 VC_ROLE.value(role_t::VCR_SEARCH));
×
2164
        }
2165
        if (this->tc_search_found && this->tc_search_found.value()) {
×
2166
            search_prompt.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2167
        }
UNCOV
2168
        search_prompt.insert(0, " Search: "_status_subtitle);
×
UNCOV
2169
        if (this->tc_search_found && !this->tc_search_found.value()) {
×
UNCOV
2170
            search_prompt.with_attr_for_all(
×
UNCOV
2171
                VC_ROLE.value(role_t::VCR_ALERT_STATUS));
×
2172
        }
UNCOV
2173
        auto lr = line_range{0, dim.dr_width};
×
2174
        mvwattrline(this->tc_window,
×
2175
                    this->vc_y + dim.dr_height - 1,
×
2176
                    this->vc_x,
2177
                    search_prompt,
2178
                    lr);
2179
    } else if (this->tc_height > 1) {
1✔
UNCOV
2180
        auto mark_iter = this->tc_marks.find(this->tc_cursor);
×
2181

2182
        if (mark_iter != this->tc_marks.end()) {
×
UNCOV
2183
            auto mark_lines = mark_iter->second.to_attr_line().split_lines();
×
2184
            auto avail_height
2185
                = std::min(dim.dr_height, (int) mark_lines.size());
×
UNCOV
2186
            auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2187
            for (auto& al : mark_lines) {
×
2188
                al.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2189
                auto lr = line_range{0, dim.dr_width};
×
2190
                mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
2191
                if (notice_y >= y_max) {
×
UNCOV
2192
                    break;
×
2193
                }
2194
            }
2195
        }
2196
    }
2197

2198
    if (this->tc_height > 1) {
1✔
UNCOV
2199
        double progress = 1.0;
×
2200
        double coverage = 1.0;
×
2201

UNCOV
2202
        if (row_count > 0) {
×
UNCOV
2203
            progress = (double) this->tc_top / (double) row_count;
×
UNCOV
2204
            coverage = (double) dim.dr_height / (double) row_count;
×
2205
        }
2206

UNCOV
2207
        auto scroll_top = (int) (progress * (double) dim.dr_height);
×
2208
        auto scroll_bottom = scroll_top
UNCOV
2209
            + std::min(dim.dr_height,
×
2210
                       (int) (coverage * (double) dim.dr_height));
×
2211

UNCOV
2212
        for (auto y = this->vc_y; y < y_max; y++) {
×
2213
            auto role = this->vc_default_role;
×
2214
            auto bar_role = role_t::VCR_SCROLLBAR;
×
2215
            auto ch = NCACS_VLINE;
×
UNCOV
2216
            if (y >= this->vc_y + scroll_top && y <= this->vc_y + scroll_bottom)
×
2217
            {
2218
                role = bar_role;
×
2219
            }
UNCOV
2220
            auto attrs = vc.attrs_for_role(role);
×
2221
            ncplane_putstr_yx(
×
2222
                this->tc_window, y, this->vc_x + dim.dr_width - 1, ch);
×
UNCOV
2223
            ncplane_set_cell_yx(this->tc_window,
×
2224
                                y,
2225
                                this->vc_x + dim.dr_width - 1,
×
UNCOV
2226
                                attrs.ta_attrs | NCSTYLE_ALTCHARSET,
×
2227
                                view_colors::to_channels(attrs));
2228
        }
2229
    }
2230

2231
    return view_curses::do_update() || retval;
1✔
2232
}
2233

2234
void
2235
textinput_curses::open_popup_for_completion(
×
2236
    line_range crange, std::vector<attr_line_t> possibilities)
2237
{
2238
    if (possibilities.empty()) {
×
2239
        this->tc_popup_type = popup_type_t::none;
×
2240
        return;
×
2241
    }
2242

2243
    this->tc_popup_type = popup_type_t::completion;
×
UNCOV
2244
    auto dim = this->get_visible_dimensions();
×
2245
    auto max_width = possibilities
UNCOV
2246
        | lnav::itertools::map(&attr_line_t::column_width)
×
2247
        | lnav::itertools::max();
×
2248

2249
    auto full_width = std::min((int) max_width.value_or(1) + 3, dim.dr_width);
×
2250
    auto new_sel = 0_vl;
×
2251
    auto popup_height = vis_line_t(
2252
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
2253
    ssize_t rel_x = crange.lr_start;
×
2254
    if (this->tc_cursor.y == 0) {
×
2255
        rel_x += this->tc_prefix.column_width();
×
2256
    }
2257
    if (rel_x + full_width > dim.dr_width) {
×
2258
        rel_x = dim.dr_width - full_width;
×
2259
    }
UNCOV
2260
    if (this->vc_x + rel_x > 0) {
×
UNCOV
2261
        rel_x -= 1;  // XXX for border
×
2262
    }
UNCOV
2263
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2264
    if (this->vc_y + rel_y < 0) {
×
2265
        rel_y = this->tc_cursor.y - this->tc_top + 1;
×
2266
    } else {
UNCOV
2267
        std::reverse(possibilities.begin(), possibilities.end());
×
UNCOV
2268
        new_sel = vis_line_t(possibilities.size() - 1);
×
2269
    }
2270

2271
    this->tc_complete_range
2272
        = selected_range::from_key(this->tc_cursor.copy_with_x(crange.lr_start),
×
2273
                                   this->tc_cursor.copy_with_x(crange.lr_end));
×
2274
    this->tc_popup_source.replace_with(possibilities);
×
2275
    this->tc_popup.set_window(this->tc_window);
×
UNCOV
2276
    this->tc_popup.set_x(this->vc_x + rel_x);
×
2277
    this->tc_popup.set_y(this->vc_y + rel_y);
×
2278
    this->tc_popup.set_width(full_width);
×
UNCOV
2279
    this->tc_popup.set_height(popup_height);
×
UNCOV
2280
    this->tc_popup.set_visible(true);
×
2281
    this->tc_popup.set_top(0_vl);
×
UNCOV
2282
    this->tc_popup.set_selection(new_sel);
×
UNCOV
2283
    this->set_needs_update();
×
2284
}
2285

2286
void
2287
textinput_curses::open_popup_for_history(std::vector<attr_line_t> possibilities)
×
2288
{
2289
    if (possibilities.empty()) {
×
2290
        this->tc_popup_type = popup_type_t::none;
×
2291
        return;
×
2292
    }
2293

2294
    this->tc_popup_type = popup_type_t::history;
×
2295
    auto new_sel = 0_vl;
×
2296
    auto popup_height = vis_line_t(
2297
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
2298
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2299
    if (this->vc_y + rel_y < 0) {
×
2300
        rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2301
    } else {
2302
        std::reverse(possibilities.begin(), possibilities.end());
×
UNCOV
2303
        new_sel = vis_line_t(possibilities.size() - 1);
×
2304
    }
2305

2306
    this->tc_complete_range = selected_range::from_key(
×
2307
        input_point::home(),
2308
        input_point{
2309
            (int) this->tc_lines.back().column_width(),
×
UNCOV
2310
            (int) this->tc_lines.size() - 1,
×
2311
        });
×
2312
    this->tc_popup_source.replace_with(possibilities);
×
2313
    this->tc_popup.set_window(this->tc_window);
×
UNCOV
2314
    this->tc_popup.set_title("History");
×
2315
    this->tc_popup.set_x(this->vc_x);
×
UNCOV
2316
    this->tc_popup.set_y(this->vc_y + rel_y);
×
UNCOV
2317
    this->tc_popup.set_width(this->vc_width);
×
2318
    this->tc_popup.set_height(popup_height);
×
2319
    this->tc_popup.set_top(0_vl);
×
UNCOV
2320
    this->tc_popup.set_selection(new_sel);
×
UNCOV
2321
    this->tc_popup.set_visible(true);
×
UNCOV
2322
    if (this->tc_on_popup_change) {
×
UNCOV
2323
        this->tc_in_popup_change = true;
×
UNCOV
2324
        this->tc_on_popup_change(*this);
×
UNCOV
2325
        this->tc_in_popup_change = false;
×
2326
    }
UNCOV
2327
    this->set_needs_update();
×
2328
}
2329

2330
void
UNCOV
2331
textinput_curses::tick(ui_clock::time_point now)
×
2332
{
UNCOV
2333
    if (this->tc_last_tick_after_input) {
×
UNCOV
2334
        auto diff = now - this->tc_last_tick_after_input.value();
×
2335

UNCOV
2336
        if (diff >= 750ms && !this->tc_timeout_fired) {
×
UNCOV
2337
            if (this->tc_on_timeout) {
×
UNCOV
2338
                this->tc_on_timeout(*this);
×
2339
            }
UNCOV
2340
            this->tc_timeout_fired = true;
×
2341
        }
2342
    } else {
2343
        this->tc_last_tick_after_input = now;
×
UNCOV
2344
        this->tc_timeout_fired = false;
×
2345
    }
2346
}
2347

2348
int
2349
textinput_curses::get_cursor_offset() const
3✔
2350
{
2351
    if (this->tc_cursor.y < 0
6✔
2352
        || this->tc_cursor.y >= (ssize_t) this->tc_lines.size())
3✔
2353
    {
2354
        // XXX can happen during update_lines() with history/pasted insert
2355
        return 0;
×
2356
    }
2357

2358
    int retval = 0;
3✔
2359
    for (auto row = 0; row < this->tc_cursor.y; row++) {
3✔
UNCOV
2360
        retval += this->tc_lines[row].al_string.size() + 1;
×
2361
    }
2362
    retval += this->tc_cursor.x;
3✔
2363

2364
    return retval;
3✔
2365
}
2366

2367
textinput_curses::input_point
UNCOV
2368
textinput_curses::get_point_for_offset(int offset) const
×
2369
{
2370
    auto retval = input_point::home();
×
2371
    auto row = size_t{0};
×
UNCOV
2372
    for (; row < this->tc_lines.size() && offset > 0; row++) {
×
UNCOV
2373
        if (offset < (ssize_t) this->tc_lines[row].al_string.size() + 1) {
×
2374
            break;
×
2375
        }
2376
        offset -= this->tc_lines[row].al_string.size() + 1;
×
2377
        retval.y += 1;
×
2378
    }
UNCOV
2379
    if (row < this->tc_lines.size()) {
×
2380
        retval.x = this->tc_lines[row].byte_to_column_index(offset);
×
2381
    }
2382

UNCOV
2383
    return retval;
×
2384
}
2385

2386
void
UNCOV
2387
textinput_curses::add_mark(input_point pos,
×
2388
                           const lnav::console::user_message& msg)
2389
{
UNCOV
2390
    if (pos.y < 0 || pos.y >= (ssize_t) this->tc_lines.size()) {
×
UNCOV
2391
        log_error("invalid mark position: %d:%d", pos.x, pos.y);
×
UNCOV
2392
        return;
×
2393
    }
2394

UNCOV
2395
    if (this->tc_marks.count(pos) > 0) {
×
UNCOV
2396
        return;
×
2397
    }
2398

UNCOV
2399
    auto& line = this->tc_lines[pos.y];
×
UNCOV
2400
    auto byte_x = (int) line.column_to_byte_index(pos.x);
×
UNCOV
2401
    auto lr = line_range{byte_x, byte_x + 1};
×
UNCOV
2402
    line.al_attrs.emplace_back(lr, VC_ROLE.value(role_t::VCR_ERROR));
×
UNCOV
2403
    line.al_attrs.emplace_back(lr, VC_STYLE.value(text_attrs::with_reverse()));
×
2404

UNCOV
2405
    this->tc_marks.emplace(pos, msg);
×
2406
}
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