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

tstack / lnav / 19092286039-2639

05 Nov 2025 05:26AM UTC coverage: 68.913% (-0.007%) from 68.92%
19092286039-2639

push

github

tstack
[session] skip restoring commands

Related to #1450

94 of 116 new or added lines in 8 files covered. (81.03%)

40 existing lines in 8 files now uncovered.

50578 of 73394 relevant lines covered (68.91%)

429603.27 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->clamp_point(this->tc_cursor);
34✔
309
    this->set_needs_update();
34✔
310
}
34✔
311

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

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

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

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

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

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

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

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

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

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

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

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

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

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

519
    return true;
×
520
}
521

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

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

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

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

650
    return false;
651
}
652

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

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

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

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

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

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

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

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

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

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

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

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

882
    if (this->tc_mode == mode_t::searching) {
4✔
883
        return this->handle_search_key(ch);
×
884
    }
885

886
    auto dim = this->get_visible_dimensions();
4✔
887
    auto inner_height = this->tc_lines.size();
4✔
888
    auto bottom = inner_height - 1;
4✔
889
    auto chid = ch.id;
4✔
890

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

903
    if (ncinput_alt_p(&ch)) {
4✔
904
        switch (chid) {
×
905
            case NCKEY_LEFT: {
×
906
                auto& al = this->tc_lines[this->tc_cursor.y];
×
907
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
908
                                        .prev_word(this->tc_cursor.x);
×
909

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

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

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

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

1190
    switch (chid) {
4✔
1191
        case NCKEY_ESC:
×
1192
        case KEY_CTRL(']'): {
1193
            if (this->tc_popup.is_visible()) {
×
1194
                if (this->tc_on_popup_cancel) {
×
1195
                    this->tc_on_popup_cancel(*this);
×
1196
                }
1197
                this->tc_popup_type = popup_type_t::none;
×
1198
                this->tc_popup.set_visible(false);
×
1199
                this->tc_complete_range = std::nullopt;
×
1200
                this->set_needs_update();
×
1201
            } else {
1202
                this->abort();
×
1203
            }
1204

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

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

1363
                if (match_opt && !match_opt->f_all.empty()
×
1364
                    && match_opt->f_all.sf_end == this->tc_cursor.x)
×
1365
                {
1366
                    auto is_comment = al.al_attrs
×
1367
                        | lnav::itertools::find_if([](const string_attr& sa) {
×
1368
                                          return (sa.sa_type == &VC_ROLE)
×
1369
                                              && sa.sa_value.get<role_t>()
×
1370
                                              == role_t::VCR_COMMENT;
×
1371
                                      });
×
1372
                    if (!is_comment && md[1]) {
×
1373
                        this->tc_selection = selected_range::from_key(
×
1374
                            this->tc_cursor.copy_with_x(md[1]->sf_begin),
×
1375
                            this->tc_cursor);
×
1376
                        auto indent = std::string(
1377
                            md[1]->startswith(">") ? 0 : md[1]->length(), ' ');
×
1378

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

1465
                    auto repl = (md[2]->front() == ' ') ? "X"_frag : " "_frag;
×
1466
                    this->replace_selection(repl);
×
1467
                    return true;
×
1468
                }
1469

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

1497
                    if (!this->tc_selection) {
3✔
1498
                        this->tc_selection
1499
                            = selected_range::from_point(this->tc_cursor);
3✔
1500
                    }
1501
                    this->replace_selection(string_fragment::from_c_str(utf8));
3✔
1502
                } else {
1503
                    this->tc_notice = unhandled_input();
×
1504
                    this->set_needs_update();
×
1505
                }
1506
            }
1507
            return true;
3✔
1508
        }
1509
    }
1510

1511
    return false;
×
1512
}
1513

1514
void
1515
textinput_curses::ensure_cursor_visible()
18✔
1516
{
1517
    if (!this->vc_enabled) {
18✔
1518
        return;
15✔
1519
    }
1520

1521
    auto dim = this->get_visible_dimensions();
3✔
1522
    auto orig_top = this->tc_top;
3✔
1523
    auto orig_left = this->tc_left;
3✔
1524
    auto orig_cursor = this->tc_cursor;
3✔
1525
    auto orig_max_cursor_x = this->tc_max_cursor_x;
3✔
1526

1527
    this->clamp_point(this->tc_cursor);
3✔
1528
    if (this->tc_cursor.y < 0) {
3✔
1529
        this->tc_cursor.y = 0;
×
1530
    }
1531
    if (this->tc_cursor.y >= (ssize_t) this->tc_lines.size()) {
3✔
1532
        this->tc_cursor.y = this->tc_lines.size() - 1;
×
1533
    }
1534
    if (this->tc_cursor.x < 0) {
3✔
1535
        this->tc_cursor.x = 0;
×
1536
    }
1537
    if (this->tc_cursor.x
6✔
1538
        >= (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1539
    {
1540
        this->tc_cursor.x = this->tc_lines[this->tc_cursor.y].column_width();
3✔
1541
    }
1542

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

1581
    if (this->tc_cursor.x
6✔
1582
        == (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1583
    {
1584
        if (this->tc_cursor.x >= this->tc_max_cursor_x) {
3✔
1585
            this->tc_max_cursor_x = this->tc_cursor.x;
3✔
1586
        }
1587
    } else {
1588
        this->tc_max_cursor_x = this->tc_cursor.x;
×
1589
    }
1590

1591
    if (orig_top != this->tc_top || orig_left != this->tc_left
3✔
1592
        || orig_cursor != this->tc_cursor
3✔
1593
        || orig_max_cursor_x != this->tc_max_cursor_x)
6✔
1594
    {
1595
        this->set_needs_update();
3✔
1596
    }
1597
}
1598

1599
void
1600
textinput_curses::apply_highlights()
3✔
1601
{
1602
    if (this->tc_text_format == text_format_t::TF_LNAV_SCRIPT) {
3✔
1603
        return;
×
1604
    }
1605

1606
    for (auto& line : this->tc_lines) {
6✔
1607
        for (const auto& hl_pair : this->tc_highlights) {
3✔
1608
            const auto& hl = hl_pair.second;
×
1609

1610
            if (!hl.applies_to_format(this->tc_text_format)) {
×
1611
                continue;
×
1612
            }
1613
            hl.annotate(line, line_range{0, -1});
×
1614
        }
1615
    }
1616
}
1617

1618
std::string
1619
textinput_curses::replace_selection_no_change(string_fragment sf)
3✔
1620
{
1621
    if (!this->tc_selection) {
3✔
1622
        return "";
×
1623
    }
1624

1625
    std::optional<int> del_max;
3✔
1626
    auto full_first_line = false;
3✔
1627
    std::string retval;
3✔
1628

1629
    auto range = std::exchange(this->tc_selection, std::nullopt).value();
3✔
1630
    this->tc_cursor.y = range.sr_start.y;
3✔
1631
    for (auto curr_line = range.sr_start.y;
6✔
1632
         curr_line <= range.sr_end.y && curr_line < this->tc_lines.size();
6✔
1633
         ++curr_line)
1634
    {
1635
        auto sel_range = range.range_for_line(curr_line);
3✔
1636

1637
        if (!sel_range) {
3✔
1638
            continue;
×
1639
        }
1640

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

1684
            retval.append(al.al_string.substr(start, end - start));
3✔
1685
            if (sel_range->lr_end == -1) {
3✔
1686
                retval.push_back('\n');
×
1687
            }
1688
            al.erase(start, end - start);
3✔
1689
            if (full_first_line || curr_line == range.sr_start.y) {
3✔
1690
                al.insert(start, sf.to_string());
3✔
1691
                this->tc_cursor.x = sel_range->lr_start;
3✔
1692
            }
1693
            if (!full_first_line && sel_range->lr_start == 0
3✔
1694
                && range.sr_start.y < curr_line && curr_line == range.sr_end.y)
6✔
1695
            {
1696
                del_max = curr_line;
×
1697
                this->tc_lines[range.sr_start.y].append(al);
×
1698
            }
1699
        }
1700
    }
1701

1702
    if (del_max) {
3✔
1703
        log_debug("deleting lines [%d+%d:%d)",
×
1704
                  range.sr_start.y,
1705
                  (full_first_line ? 0 : 1),
1706
                  del_max.value() + 1);
1707
        this->tc_lines.erase(this->tc_lines.begin() + range.sr_start.y
×
1708
                                 + (full_first_line ? 0 : 1),
×
1709
                             this->tc_lines.begin() + del_max.value() + 1);
×
1710
    }
1711

1712
    const auto repl_last_line
1713
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'});
3✔
1714
    log_debug(
3✔
1715
        "last line '%.*s'", repl_last_line.length(), repl_last_line.data());
1716
    const auto repl_cols
1717
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'})
3✔
1718
              .column_width();
3✔
1719
    const auto repl_lines = sf.count('\n');
3✔
1720
    log_debug("repl_cols => %d", repl_cols);
3✔
1721
    if (repl_lines > 0) {
3✔
1722
        this->tc_cursor.x = repl_cols;
×
1723
    } else {
1724
        this->tc_cursor.x += repl_cols;
3✔
1725
    }
1726
    this->tc_cursor.y += repl_lines;
3✔
1727

1728
    this->tc_drag_selection = std::nullopt;
3✔
1729
    if (retval == sf) {
3✔
1730
        if (!sf.empty()) {
×
1731
            this->content_to_lines(this->get_content(),
×
1732
                                   this->get_cursor_offset());
1733
        }
1734
    } else {
1735
        this->update_lines();
3✔
1736
    }
1737

1738
    ensure(!this->tc_lines.empty());
3✔
1739

1740
    return retval;
3✔
1741
}
3✔
1742

1743
void
1744
textinput_curses::replace_selection(string_fragment sf)
3✔
1745
{
1746
    static constexpr uint32_t mask
1747
        = UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N | UC_CATEGORY_MASK_Pc;
1748

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

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

1842
void
1843
textinput_curses::move_cursor_to(input_point ip)
×
1844
{
1845
    this->tc_cursor = ip;
×
1846
    if (this->tc_drag_selection) {
×
1847
        this->tc_drag_selection = std::nullopt;
×
1848
        this->set_needs_update();
×
1849
    }
1850
    if (this->tc_selection) {
×
1851
        this->tc_selection = std::nullopt;
×
1852
        this->set_needs_update();
×
1853
    }
1854
    this->ensure_cursor_visible();
×
1855
}
1856

1857
void
1858
textinput_curses::update_lines()
3✔
1859
{
1860
    const auto x = this->get_cursor_offset();
3✔
1861
    this->content_to_lines(this->get_content(), x);
3✔
1862
    this->set_needs_update();
3✔
1863
    this->ensure_cursor_visible();
3✔
1864

1865
    this->tc_marks.clear();
3✔
1866
    if (this->tc_in_popup_change) {
3✔
1867
        log_trace("in popup change, skipping");
×
1868
    } else {
1869
        this->tc_popup.set_visible(false);
3✔
1870
        this->tc_complete_range = std::nullopt;
3✔
1871
        if (this->tc_on_change) {
3✔
1872
            this->tc_on_change(*this);
3✔
1873
        }
1874
        if (!this->tc_popup.is_visible()) {
3✔
1875
            this->tc_popup_type = popup_type_t::none;
3✔
1876
        }
1877
    }
1878

1879
    ensure(!this->tc_lines.empty());
3✔
1880
}
3✔
1881

1882
textinput_curses::dimension_result
1883
textinput_curses::get_visible_dimensions() const
23✔
1884
{
1885
    dimension_result retval;
23✔
1886

1887
    ncplane_dim_yx(
23✔
1888
        this->tc_window, &retval.dr_full_height, &retval.dr_full_width);
23✔
1889

1890
    if (this->vc_y < (ssize_t) retval.dr_full_height) {
23✔
1891
        retval.dr_height = std::min((int) retval.dr_full_height - this->vc_y,
23✔
1892
                                    this->tc_height);
23✔
1893
    }
1894
    if (this->vc_x < (ssize_t) retval.dr_full_width) {
23✔
1895
        retval.dr_width = std::min((long) retval.dr_full_width - this->vc_x,
23✔
1896
                                   this->vc_width);
23✔
1897
    }
1898
    return retval;
23✔
1899
}
1900

1901
std::string
1902
textinput_curses::get_content(bool trim) const
7✔
1903
{
1904
    auto need_lf = false;
7✔
1905
    std::string retval;
7✔
1906

1907
    for (const auto& al : this->tc_lines) {
14✔
1908
        const auto& line = al.al_string;
7✔
1909
        auto line_sf = string_fragment::from_str(line);
7✔
1910
        if (trim) {
7✔
1911
            line_sf = line_sf.rtrim(" ");
×
1912
        }
1913
        if (need_lf) {
7✔
1914
            retval.push_back('\n');
×
1915
        }
1916
        retval += line_sf;
7✔
1917
        need_lf = true;
7✔
1918
    }
1919
    return retval;
7✔
1920
}
×
1921

1922
void
1923
textinput_curses::focus()
2✔
1924
{
1925
    if (!this->vc_enabled) {
2✔
1926
        this->vc_enabled = true;
1✔
1927
        if (this->tc_on_focus) {
1✔
1928
            this->tc_on_focus(*this);
×
1929
        }
1930
        this->set_needs_update();
1✔
1931
    }
1932

1933
    if (this->tc_mode == mode_t::show_help
4✔
1934
        || (this->tc_height && this->tc_notice)
2✔
1935
        || (this->tc_selection
4✔
1936
            && this->tc_selection->contains_exclusive(this->tc_cursor)))
2✔
1937
    {
1938
        notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
×
1939
        return;
×
1940
    }
1941
    auto term_x = this->vc_x + this->tc_cursor.x - this->tc_left;
2✔
1942
    if (this->tc_cursor.y == 0) {
2✔
1943
        term_x += this->tc_prefix.column_width();
2✔
1944
    }
1945
    notcurses_cursor_enable(ncplane_notcurses(this->tc_window),
2✔
1946
                            this->vc_y + this->tc_cursor.y - this->tc_top,
2✔
1947
                            term_x);
1948
}
1949

1950
void
1951
textinput_curses::blur()
1✔
1952
{
1953
    this->tc_popup_type = popup_type_t::none;
1✔
1954
    this->tc_popup.set_visible(false);
1✔
1955
    this->vc_enabled = false;
1✔
1956
    if (this->tc_on_blur) {
1✔
1957
        this->tc_on_blur(*this);
1✔
1958
    }
1959

1960
    notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
1✔
1961
    this->set_needs_update();
1✔
1962
}
1✔
1963

1964
void
1965
textinput_curses::abort()
×
1966
{
1967
    this->blur();
×
1968
    this->tc_selection = std::nullopt;
×
1969
    this->tc_drag_selection = std::nullopt;
×
1970
    if (this->tc_on_abort) {
×
1971
        this->tc_on_abort(*this);
×
1972
    }
1973
}
1974

1975
void
1976
textinput_curses::sync_to_sysclip() const
×
1977
{
1978
    auto clip_open_res = sysclip::open(sysclip::type_t::GENERAL);
×
1979

1980
    if (clip_open_res.isOk()) {
×
1981
        auto clip_file = clip_open_res.unwrap();
×
1982
        fmt::print(clip_file.in(),
×
1983
                   FMT_STRING("{}"),
×
1984
                   fmt::join(this->tc_clipboard, ""));
×
1985
    } else {
×
1986
        auto err_msg = clip_open_res.unwrapErr();
×
1987
        log_error("unable to open clipboard: %s", err_msg.c_str());
×
1988
    }
1989
}
1990

1991
bool
1992
textinput_curses::do_update()
23✔
1993
{
1994
    static auto& vc = view_colors::singleton();
23✔
1995
    auto retval = false;
23✔
1996

1997
    if (!this->is_visible()) {
23✔
UNCOV
1998
        return retval;
×
1999
    }
2000

2001
    auto popup_height = this->tc_popup.get_height();
23✔
2002
    auto rel_y = (this->tc_popup_type == popup_type_t::history
23✔
2003
                      ? 0
23✔
2004
                      : this->tc_cursor.y - this->tc_top)
23✔
2005
        - popup_height;
23✔
2006
    if (this->vc_y + rel_y < 0) {
23✔
2007
        rel_y = this->tc_cursor.y - this->tc_top + popup_height + 1;
×
2008
    }
2009
    this->tc_popup.set_y(this->vc_y + rel_y);
23✔
2010

2011
    if (!this->vc_needs_update) {
23✔
2012
        return view_curses::do_update();
7✔
2013
    }
2014

2015
    auto dim = this->get_visible_dimensions();
16✔
2016
    if (!this->vc_enabled) {
16✔
2017
        ncplane_erase_region(
15✔
2018
            this->tc_window, this->vc_y, this->vc_x, 1, dim.dr_width);
2019
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
15✔
2020
        mvwattrline(this->tc_window,
×
2021
                    this->vc_y,
2022
                    this->vc_x,
2023
                    this->tc_inactive_value,
15✔
2024
                    lr);
2025

2026
        if (!this->tc_alt_value.empty()
15✔
2027
            && (ssize_t) (this->tc_inactive_value.column_width() + 3
19✔
2028
                          + this->tc_alt_value.column_width())
4✔
2029
                < dim.dr_width)
4✔
2030
        {
2031
            auto alt_x = dim.dr_width - this->tc_alt_value.column_width();
4✔
2032
            auto lr = line_range{0, (int) this->tc_alt_value.column_width()};
4✔
2033
            mvwattrline(
4✔
2034
                this->tc_window, this->vc_y, alt_x, this->tc_alt_value, lr);
4✔
2035
        }
2036

2037
        this->vc_needs_update = false;
15✔
2038
        return true;
15✔
2039
    }
2040

2041
    if (this->tc_mode == mode_t::show_help) {
1✔
2042
        this->tc_help_view.set_window(this->tc_window);
×
2043
        this->tc_help_view.set_x(this->vc_x);
×
2044
        this->tc_help_view.set_y(this->vc_y);
×
2045
        this->tc_help_view.set_width(this->vc_width);
×
2046
        this->tc_help_view.set_height(vis_line_t(this->tc_height));
×
2047
        this->tc_help_view.set_visible(true);
×
2048
        return view_curses::do_update();
×
2049
    }
2050

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

2114
        auto al = attr_line_t();
×
2115
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
2116
        mvwattrline(
×
2117
            this->tc_window, y, this->vc_x, al, EMPTY_LR, role_t::VCR_ALT_ROW);
2118
    }
2119
    if (this->tc_notice) {
1✔
2120
        auto notice_lines = this->tc_notice.value();
×
2121
        auto avail_height = std::min(dim.dr_height, (int) notice_lines.size());
×
2122
        auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2123

2124
        for (auto& al : notice_lines) {
×
2125
            auto lr = line_range{0, dim.dr_width};
×
2126
            mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
2127
            if (notice_y >= y_max) {
×
2128
                break;
×
2129
            }
2130
        }
2131
    } else if (this->tc_mode == mode_t::searching) {
1✔
2132
        auto search_prompt = attr_line_t(" ");
×
2133
        if (this->tc_search.empty() || this->tc_search_found.has_value()) {
×
2134
            search_prompt.append(this->tc_search)
×
2135
                .append(" ", VC_ROLE.value(role_t::VCR_CURSOR_LINE));
×
2136
        } else {
2137
            search_prompt.append(this->tc_search,
×
2138
                                 VC_ROLE.value(role_t::VCR_SEARCH));
×
2139
        }
2140
        if (this->tc_search_found && this->tc_search_found.value()) {
×
2141
            search_prompt.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2142
        }
2143
        search_prompt.insert(0, " Search: "_status_subtitle);
×
2144
        if (this->tc_search_found && !this->tc_search_found.value()) {
×
2145
            search_prompt.with_attr_for_all(
×
2146
                VC_ROLE.value(role_t::VCR_ALERT_STATUS));
×
2147
        }
2148
        auto lr = line_range{0, dim.dr_width};
×
2149
        mvwattrline(this->tc_window,
×
2150
                    this->vc_y + dim.dr_height - 1,
×
2151
                    this->vc_x,
2152
                    search_prompt,
2153
                    lr);
2154
    } else if (this->tc_height > 1) {
1✔
2155
        auto mark_iter = this->tc_marks.find(this->tc_cursor);
×
2156

2157
        if (mark_iter != this->tc_marks.end()) {
×
2158
            auto mark_lines = mark_iter->second.to_attr_line().split_lines();
×
2159
            auto avail_height
2160
                = std::min(dim.dr_height, (int) mark_lines.size());
×
2161
            auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2162
            for (auto& al : mark_lines) {
×
2163
                al.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2164
                auto lr = line_range{0, dim.dr_width};
×
2165
                mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
2166
                if (notice_y >= y_max) {
×
2167
                    break;
×
2168
                }
2169
            }
2170
        }
2171
    }
2172

2173
    if (this->tc_height > 1) {
1✔
2174
        double progress = 1.0;
×
2175
        double coverage = 1.0;
×
2176

2177
        if (row_count > 0) {
×
2178
            progress = (double) this->tc_top / (double) row_count;
×
2179
            coverage = (double) dim.dr_height / (double) row_count;
×
2180
        }
2181

2182
        auto scroll_top = (int) (progress * (double) dim.dr_height);
×
2183
        auto scroll_bottom = scroll_top
2184
            + std::min(dim.dr_height,
×
2185
                       (int) (coverage * (double) dim.dr_height));
×
2186

2187
        for (auto y = this->vc_y; y < y_max; y++) {
×
2188
            auto role = this->vc_default_role;
×
2189
            auto bar_role = role_t::VCR_SCROLLBAR;
×
2190
            auto ch = NCACS_VLINE;
×
2191
            if (y >= this->vc_y + scroll_top && y <= this->vc_y + scroll_bottom)
×
2192
            {
2193
                role = bar_role;
×
2194
            }
2195
            auto attrs = vc.attrs_for_role(role);
×
2196
            ncplane_putstr_yx(
×
2197
                this->tc_window, y, this->vc_x + dim.dr_width - 1, ch);
×
2198
            ncplane_set_cell_yx(this->tc_window,
×
2199
                                y,
2200
                                this->vc_x + dim.dr_width - 1,
×
2201
                                attrs.ta_attrs | NCSTYLE_ALTCHARSET,
×
2202
                                view_colors::to_channels(attrs));
2203
        }
2204
    }
2205

2206
    return view_curses::do_update() || retval;
1✔
2207
}
2208

2209
void
2210
textinput_curses::open_popup_for_completion(
×
2211
    line_range crange, std::vector<attr_line_t> possibilities)
2212
{
2213
    if (possibilities.empty()) {
×
2214
        this->tc_popup_type = popup_type_t::none;
×
2215
        return;
×
2216
    }
2217

2218
    this->tc_popup_type = popup_type_t::completion;
×
2219
    auto dim = this->get_visible_dimensions();
×
2220
    auto max_width = possibilities
2221
        | lnav::itertools::map(&attr_line_t::column_width)
×
2222
        | lnav::itertools::max();
×
2223

2224
    auto full_width = std::min((int) max_width.value_or(1) + 3, dim.dr_width);
×
2225
    auto new_sel = 0_vl;
×
2226
    auto popup_height = vis_line_t(
2227
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
2228
    ssize_t rel_x = crange.lr_start;
×
2229
    if (this->tc_cursor.y == 0) {
×
2230
        rel_x += this->tc_prefix.column_width();
×
2231
    }
2232
    if (rel_x + full_width > dim.dr_width) {
×
2233
        rel_x = dim.dr_width - full_width;
×
2234
    }
2235
    if (this->vc_x + rel_x > 0) {
×
2236
        rel_x -= 1;  // XXX for border
×
2237
    }
2238
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2239
    if (this->vc_y + rel_y < 0) {
×
2240
        rel_y = this->tc_cursor.y - this->tc_top + 1;
×
2241
    } else {
2242
        std::reverse(possibilities.begin(), possibilities.end());
×
2243
        new_sel = vis_line_t(possibilities.size() - 1);
×
2244
    }
2245

2246
    this->tc_complete_range
2247
        = selected_range::from_key(this->tc_cursor.copy_with_x(crange.lr_start),
×
2248
                                   this->tc_cursor.copy_with_x(crange.lr_end));
×
2249
    this->tc_popup_source.replace_with(possibilities);
×
2250
    this->tc_popup.set_window(this->tc_window);
×
2251
    this->tc_popup.set_x(this->vc_x + rel_x);
×
2252
    this->tc_popup.set_y(this->vc_y + rel_y);
×
2253
    this->tc_popup.set_width(full_width);
×
2254
    this->tc_popup.set_height(popup_height);
×
2255
    this->tc_popup.set_visible(true);
×
2256
    this->tc_popup.set_top(0_vl);
×
2257
    this->tc_popup.set_selection(new_sel);
×
2258
    this->set_needs_update();
×
2259
}
2260

2261
void
2262
textinput_curses::open_popup_for_history(std::vector<attr_line_t> possibilities)
×
2263
{
2264
    if (possibilities.empty()) {
×
2265
        this->tc_popup_type = popup_type_t::none;
×
2266
        return;
×
2267
    }
2268

2269
    this->tc_popup_type = popup_type_t::history;
×
2270
    auto new_sel = 0_vl;
×
2271
    auto popup_height = vis_line_t(
2272
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
2273
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2274
    if (this->vc_y + rel_y < 0) {
×
2275
        rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2276
    } else {
2277
        std::reverse(possibilities.begin(), possibilities.end());
×
2278
        new_sel = vis_line_t(possibilities.size() - 1);
×
2279
    }
2280

2281
    this->tc_complete_range = selected_range::from_key(
×
2282
        input_point::home(),
2283
        input_point{
2284
            (int) this->tc_lines.back().column_width(),
×
2285
            (int) this->tc_lines.size() - 1,
×
2286
        });
×
2287
    this->tc_popup_source.replace_with(possibilities);
×
2288
    this->tc_popup.set_window(this->tc_window);
×
2289
    this->tc_popup.set_title("History");
×
2290
    this->tc_popup.set_x(this->vc_x);
×
2291
    this->tc_popup.set_y(this->vc_y + rel_y);
×
2292
    this->tc_popup.set_width(this->vc_width);
×
2293
    this->tc_popup.set_height(popup_height);
×
2294
    this->tc_popup.set_top(0_vl);
×
2295
    this->tc_popup.set_selection(new_sel);
×
2296
    this->tc_popup.set_visible(true);
×
2297
    if (this->tc_on_popup_change) {
×
2298
        this->tc_in_popup_change = true;
×
2299
        this->tc_on_popup_change(*this);
×
2300
        this->tc_in_popup_change = false;
×
2301
    }
2302
    this->set_needs_update();
×
2303
}
2304

2305
void
2306
textinput_curses::tick(ui_clock::time_point now)
×
2307
{
2308
    if (this->tc_last_tick_after_input) {
×
2309
        auto diff = now - this->tc_last_tick_after_input.value();
×
2310

2311
        if (diff >= 750ms && !this->tc_timeout_fired) {
×
2312
            if (this->tc_on_timeout) {
×
2313
                this->tc_on_timeout(*this);
×
2314
            }
2315
            this->tc_timeout_fired = true;
×
2316
        }
2317
    } else {
2318
        this->tc_last_tick_after_input = now;
×
2319
        this->tc_timeout_fired = false;
×
2320
    }
2321
}
2322

2323
int
2324
textinput_curses::get_cursor_offset() const
3✔
2325
{
2326
    if (this->tc_cursor.y < 0
6✔
2327
        || this->tc_cursor.y >= (ssize_t) this->tc_lines.size())
3✔
2328
    {
2329
        // XXX can happen during update_lines() with history/pasted insert
2330
        return 0;
×
2331
    }
2332

2333
    int retval = 0;
3✔
2334
    for (auto row = 0; row < this->tc_cursor.y; row++) {
3✔
2335
        retval += this->tc_lines[row].al_string.size() + 1;
×
2336
    }
2337
    retval += this->tc_cursor.x;
3✔
2338

2339
    return retval;
3✔
2340
}
2341

2342
textinput_curses::input_point
2343
textinput_curses::get_point_for_offset(int offset) const
×
2344
{
2345
    auto retval = input_point::home();
×
2346
    auto row = size_t{0};
×
2347
    for (; row < this->tc_lines.size() && offset > 0; row++) {
×
2348
        if (offset < (ssize_t) this->tc_lines[row].al_string.size() + 1) {
×
2349
            break;
×
2350
        }
2351
        offset -= this->tc_lines[row].al_string.size() + 1;
×
2352
        retval.y += 1;
×
2353
    }
2354
    if (row < this->tc_lines.size()) {
×
2355
        retval.x = this->tc_lines[row].byte_to_column_index(offset);
×
2356
    }
2357

2358
    return retval;
×
2359
}
2360

2361
void
2362
textinput_curses::add_mark(input_point pos,
×
2363
                           const lnav::console::user_message& msg)
2364
{
2365
    if (pos.y < 0 || pos.y >= (ssize_t) this->tc_lines.size()) {
×
2366
        log_error("invalid mark position: %d:%d", pos.x, pos.y);
×
2367
        return;
×
2368
    }
2369

2370
    if (this->tc_marks.count(pos) > 0) {
×
2371
        return;
×
2372
    }
2373

2374
    auto& line = this->tc_lines[pos.y];
×
2375
    auto byte_x = (int) line.column_to_byte_index(pos.x);
×
2376
    auto lr = line_range{byte_x, byte_x + 1};
×
2377
    line.al_attrs.emplace_back(lr, VC_ROLE.value(role_t::VCR_ERROR));
×
2378
    line.al_attrs.emplace_back(lr, VC_STYLE.value(text_attrs::with_reverse()));
×
2379

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