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

tstack / lnav / 19983920410-2735

05 Dec 2025 11:01PM UTC coverage: 68.917% (+0.02%) from 68.901%
19983920410-2735

push

github

tstack
[headless] make the warning for non-UTF files nicer

72 of 95 new or added lines in 9 files covered. (75.79%)

51507 of 74738 relevant lines covered (68.92%)

435484.41 hits per line

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

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

30
#include <algorithm>
31

32
#include "textinput_curses.hh"
33

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

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

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

166
    return retval;
33✔
167
}
168

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

179
    return retval;
×
180
}
181

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

192
    return retval;
×
193
}
194

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

205
    return retval;
×
206
}
207

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

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

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

228
        return false;
×
229
    }
230

231
    textinput_curses* tmd_input;
232
};
233

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

520
    return true;
×
521
}
522

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

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

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

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

651
    return false;
652
}
653

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1470
                    auto repl = (md[2]->front() == ' ') ? "X"_frag : " "_frag;
×
1471
                    this->replace_selection(repl);
×
1472
                    return true;
×
1473
                }
1474

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

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

1516
    return false;
×
1517
}
1518

1519
void
1520
textinput_curses::ensure_cursor_visible()
17✔
1521
{
1522
    if (!this->vc_enabled) {
17✔
1523
        return;
14✔
1524
    }
1525

1526
    auto dim = this->get_visible_dimensions();
3✔
1527
    auto orig_top = this->tc_top;
3✔
1528
    auto orig_left = this->tc_left;
3✔
1529
    auto orig_cursor = this->tc_cursor;
3✔
1530
    auto orig_max_cursor_x = this->tc_max_cursor_x;
3✔
1531

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

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

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

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

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

1611
    for (auto& line : this->tc_lines) {
6✔
1612
        for (const auto& hl_pair : this->tc_highlights) {
3✔
1613
            const auto& hl = hl_pair.second;
×
1614

1615
            if (!hl.applies_to_format(this->tc_text_format)) {
×
1616
                continue;
×
1617
            }
1618
            hl.annotate(line, line_range{0, -1});
×
1619
        }
1620
    }
1621
}
1622

1623
std::string
1624
textinput_curses::replace_selection_no_change(string_fragment sf)
3✔
1625
{
1626
    if (!this->tc_selection) {
3✔
1627
        return "";
×
1628
    }
1629

1630
    std::optional<int> del_max;
3✔
1631
    auto full_first_line = false;
3✔
1632
    std::string retval;
3✔
1633

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

1642
        if (!sel_range) {
3✔
1643
            continue;
×
1644
        }
1645

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

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

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

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

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

1743
    ensure(!this->tc_lines.empty());
3✔
1744

1745
    return retval;
3✔
1746
}
3✔
1747

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

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

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

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

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

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

1884
    ensure(!this->tc_lines.empty());
3✔
1885
}
3✔
1886

1887
textinput_curses::dimension_result
1888
textinput_curses::get_visible_dimensions() const
23✔
1889
{
1890
    dimension_result retval;
23✔
1891

1892
    ncplane_dim_yx(
23✔
1893
        this->tc_window, &retval.dr_full_height, &retval.dr_full_width);
23✔
1894

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

1906
std::string
1907
textinput_curses::get_content(bool trim) const
7✔
1908
{
1909
    auto need_lf = false;
7✔
1910
    std::string retval;
7✔
1911

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

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

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

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

1965
    notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
1✔
1966
    this->set_needs_update();
1✔
1967
}
1✔
1968

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

1980
void
1981
textinput_curses::sync_to_sysclip() const
×
1982
{
1983
    auto clip_open_res = sysclip::open(sysclip::type_t::GENERAL);
×
1984

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

1996
bool
1997
textinput_curses::do_update()
24✔
1998
{
1999
    static auto& vc = view_colors::singleton();
24✔
2000
    auto retval = false;
24✔
2001

2002
    if (!this->is_visible()) {
24✔
2003
        return retval;
1✔
2004
    }
2005

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

2016
    if (!this->vc_needs_update) {
23✔
2017
        return view_curses::do_update();
7✔
2018
    }
2019

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

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

2042
        this->vc_needs_update = false;
15✔
2043
        return true;
15✔
2044
    }
2045

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

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

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

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

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

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

2195
    if (this->tc_height > 1) {
1✔
2196
        double progress = 1.0;
×
2197
        double coverage = 1.0;
×
2198

2199
        if (row_count > 0) {
×
2200
            progress = (double) this->tc_top / (double) row_count;
×
2201
            coverage = (double) dim.dr_height / (double) row_count;
×
2202
        }
2203

2204
        auto scroll_top = (int) (progress * (double) dim.dr_height);
×
2205
        auto scroll_bottom = scroll_top
2206
            + std::min(dim.dr_height,
×
2207
                       (int) (coverage * (double) dim.dr_height));
×
2208

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

2228
    return view_curses::do_update() || retval;
1✔
2229
}
2230

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

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

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

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

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

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

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

2327
void
2328
textinput_curses::tick(ui_clock::time_point now)
×
2329
{
2330
    if (this->tc_last_tick_after_input) {
×
2331
        auto diff = now - this->tc_last_tick_after_input.value();
×
2332

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

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

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

2361
    return retval;
3✔
2362
}
2363

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

2380
    return retval;
×
2381
}
2382

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

2392
    if (this->tc_marks.count(pos) > 0) {
×
2393
        return;
×
2394
    }
2395

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

2402
    this->tc_marks.emplace(pos, msg);
×
2403
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc