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

tstack / lnav / 25088628874-3010

29 Apr 2026 02:55AM UTC coverage: 69.252% (+0.02%) from 69.232%
25088628874-3010

push

github

tstack
[textinput] more hotkeys

231 of 399 new or added lines in 4 files covered. (57.89%)

7 existing lines in 3 files now uncovered.

54316 of 78432 relevant lines covered (69.25%)

565770.39 hits per line

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

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

30
#include <algorithm>
31

32
#include "textinput_curses.hh"
33

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

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

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

193
    return retval;
37✔
194
}
195

196
const std::vector<attr_line_t>&
197
textinput_curses::unhandled_input()
×
198
{
199
    static const auto retval = std::vector{
200
        attr_line_t()
×
201
            .append(" Notice: "_status_subtitle)
×
202
            .append(" Unhandled key press.  Press F1 for help")
×
203
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_ALERT_STATUS)),
×
204
    };
205

206
    return retval;
×
207
}
208

209
const std::vector<attr_line_t>&
210
textinput_curses::no_changes()
×
211
{
212
    static const auto retval = std::vector{
213
        attr_line_t()
×
214
            .append(" Notice: "_status_subtitle)
×
215
            .append(" No changes to undo")
×
216
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS)),
×
217
    };
218

219
    return retval;
×
220
}
221

222
const std::vector<attr_line_t>&
223
textinput_curses::external_edit_failed()
×
224
{
225
    static const auto retval = std::vector{
226
        attr_line_t()
×
227
            .append(" Error: "_status_subtitle)
×
228
            .append(" Unable to write file for external edit")
×
229
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_ALERT_STATUS)),
×
230
    };
231

232
    return retval;
×
233
}
234

235
class textinput_mouse_delegate : public text_delegate {
236
public:
237
    textinput_mouse_delegate(textinput_curses* input) : tmd_input(input) {}
37✔
238

239
    bool text_handle_mouse(textview_curses& tc,
×
240
                           const listview_curses::display_line_content_t& dlc,
241
                           mouse_event& me) override
242
    {
243
        if (me.me_button == mouse_button_t::BUTTON_LEFT
×
244
            && me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED
×
245
            && dlc.is<listview_curses::main_content>())
×
246
        {
247
            ncinput ch{};
×
248

249
            ch.id = NCKEY_TAB;
×
250
            ch.eff_text[0] = '\t';
×
251
            ch.eff_text[1] = '\0';
×
252
            return this->tmd_input->handle_key(ch);
×
253
        }
254

255
        return false;
×
256
    }
257

258
    textinput_curses* tmd_input;
259
};
260

261
std::optional<line_range>
262
textinput_curses::selected_range::range_for_line(int y) const
3✔
263
{
264
    if (!this->contains_line(y)) {
3✔
265
        return std::nullopt;
×
266
    }
267

268
    line_range retval;
3✔
269

270
    if (y > this->sr_start.y) {
3✔
271
        retval.lr_start = 0;
×
272
    } else {
273
        retval.lr_start = this->sr_start.x;
3✔
274
    }
275
    if (y < this->sr_end.y) {
3✔
276
        retval.lr_end = -1;
×
277
    } else {
278
        retval.lr_end = this->sr_end.x;
3✔
279
    }
280
    retval.lr_unit = line_range::unit::codepoint;
3✔
281

282
    return retval;
3✔
283
}
284

285
textinput_curses::textinput_curses()
37✔
286
{
287
    this->vc_enabled = false;
37✔
288
    this->vc_children.emplace_back(&this->tc_popup);
37✔
289

290
    this->tc_popup.tc_cursor_role = role_t::VCR_CURSOR_LINE;
37✔
291
    this->tc_popup.tc_disabled_cursor_role = role_t::VCR_DISABLED_CURSOR_LINE;
37✔
292
    this->tc_popup.lv_border_left_role = role_t::VCR_POPUP_BORDER;
37✔
293
    this->tc_popup.set_visible(false);
37✔
294
    this->tc_popup.set_title("textinput popup");
74✔
295
    this->tc_popup.set_head_space(0_vl);
37✔
296
    this->tc_popup.set_selectable(true);
37✔
297
    this->tc_popup.set_show_scrollbar(true);
37✔
298
    this->tc_popup.set_default_role(role_t::VCR_POPUP);
37✔
299
    this->tc_popup.set_sub_source(&this->tc_popup_source);
37✔
300
    this->tc_popup.set_delegate(
37✔
301
        std::make_shared<textinput_mouse_delegate>(this));
74✔
302

303
    this->vc_children.emplace_back(&this->tc_help_view);
37✔
304
    this->tc_help_view.set_visible(false);
37✔
305
    this->tc_help_view.set_title("textinput help");
74✔
306
    this->tc_help_view.set_show_scrollbar(true);
37✔
307
    this->tc_help_view.set_default_role(role_t::VCR_STATUS);
37✔
308
    this->tc_help_view.set_sub_source(&this->tc_help_source);
37✔
309

310
    this->tc_on_help = [](textinput_curses& ti) {
37✔
311
        ti.tc_mode = mode_t::show_help;
×
312
        ti.set_needs_update();
×
313
    };
37✔
314
    this->tc_help_source.replace_with(get_help_text());
37✔
315

316
    this->set_content("");
37✔
317
}
37✔
318

319
void
320
textinput_curses::content_to_lines(std::string content, int x)
42✔
321
{
322
    auto al = attr_line_t(content);
42✔
323

324
    if (!this->tc_prefix.empty()) {
42✔
325
        al.insert(0, this->tc_prefix);
×
326
        x += this->tc_prefix.length();
×
327
    }
328
    highlight_syntax(this->tc_text_format, al, x);
42✔
329
    if (!this->tc_prefix.empty()) {
42✔
330
        // XXX yuck
331
        al.erase(0, this->tc_prefix.al_string.size());
×
332
    }
333
    this->tc_doc_meta = lnav::document::discover(al)
42✔
334
                            .with_text_format(this->tc_text_format)
42✔
335
                            .save_words()
42✔
336
                            .perform();
42✔
337
    this->tc_lines = al.split_lines();
42✔
338
    if (endswith(al.al_string, "\n")) {
42✔
339
        this->tc_lines.emplace_back();
×
340
    }
341
    if (this->tc_lines.empty()) {
42✔
342
        this->tc_lines.emplace_back();
39✔
343
    } else {
344
        this->apply_highlights();
3✔
345
    }
346
}
42✔
347

348
void
349
textinput_curses::set_content(std::string content)
39✔
350
{
351
    this->content_to_lines(std::move(content), this->tc_prefix.length());
39✔
352

353
    this->tc_change_log.clear();
39✔
354
    this->tc_marks.clear();
39✔
355
    this->tc_notice = std::nullopt;
39✔
356
    this->tc_left = 0;
39✔
357
    this->tc_top = 0;
39✔
358
    this->tc_cursor = {};
39✔
359
    this->tc_abort_requested = false;
39✔
360
    this->tc_drag_selection = std::nullopt;
39✔
361
    this->tc_selection = std::nullopt;
39✔
362
    this->clamp_point(this->tc_cursor);
39✔
363
    this->set_needs_update();
39✔
364
}
39✔
365

366
void
367
textinput_curses::set_height(int height)
1✔
368
{
369
    if (this->tc_height == height) {
1✔
370
        return;
1✔
371
    }
372

373
    this->tc_height = height;
×
374
    if (this->tc_height == 1) {
×
375
        if (this->tc_cursor.y != 0) {
×
376
            this->move_cursor_to(this->tc_cursor.copy_with_y(0));
×
377
        }
378
    }
379
    this->set_needs_update();
×
380
}
381

382
std::optional<view_curses*>
383
textinput_curses::contains(int x, int y)
×
384
{
385
    if (!this->vc_visible) {
×
386
        return std::nullopt;
×
387
    }
388

389
    auto child = view_curses::contains(x, y);
×
390
    if (child) {
×
391
        return child;
×
392
    }
393

394
    if (this->vc_x <= x && x < this->vc_x + this->vc_width && this->vc_y <= y
×
395
        && y < this->vc_y + this->tc_height)
×
396
    {
397
        return this;
×
398
    }
399
    return std::nullopt;
×
400
}
401

402
bool
403
textinput_curses::handle_mouse(mouse_event& me)
×
404
{
405
    ssize_t inner_height = this->tc_lines.size();
×
406

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

462
        this->tc_popup_type = popup_type_t::none;
×
463
        this->tc_popup.set_visible(false);
×
464
        this->tc_complete_range = std::nullopt;
×
465
        this->tc_cursor = inner_point;
×
466
        log_debug("new cursor x=%d y=%d", this->tc_cursor.x, this->tc_cursor.y);
×
467
        if (me.me_state == mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK) {
×
468
            const auto& al = this->tc_lines[this->tc_cursor.y];
×
469
            auto sf = string_fragment::from_str(al.al_string);
×
470
            auto cursor_sf = sf.sub_cell_range(this->tc_left + adj_x,
×
471
                                               this->tc_left + adj_x);
×
472
            auto ds = data_scanner(sf);
×
473

474
            while (true) {
475
                auto tok_res = ds.tokenize2(this->tc_text_format);
×
476
                if (!tok_res.has_value()) {
×
477
                    break;
×
478
                }
479

480
                auto tok = tok_res.value();
×
481
                log_debug("tok %d", tok.tr_token);
×
482

483
                auto tok_sf = (tok.tr_token == data_token_t::DT_QUOTED_STRING
×
484
                               && (cursor_sf.sf_begin
×
485
                                       == tok.to_string_fragment().sf_begin
×
486
                                   || cursor_sf.sf_begin
×
487
                                       == tok.to_string_fragment().sf_end - 1))
×
488
                    ? tok.to_string_fragment()
×
489
                    : tok.inner_string_fragment();
×
490
                log_debug("tok %d:%d  curs %d:%d",
×
491
                          tok_sf.sf_begin,
492
                          tok_sf.sf_end,
493
                          cursor_sf.sf_begin,
494
                          cursor_sf.sf_end);
495
                if (tok_sf.contains(cursor_sf)
×
496
                    && tok.tr_token != data_token_t::DT_WHITE)
×
497
                {
498
                    log_debug("hit!");
×
499
                    auto group_tok
500
                        = ds.find_matching_bracket(this->tc_text_format, tok);
×
501
                    if (group_tok) {
×
502
                        tok_sf = group_tok.value().to_string_fragment();
×
503
                    }
504
                    auto tok_start = input_point{
505
                        (int) sf.byte_to_column_index(tok_sf.sf_begin)
×
506
                            - this->tc_left,
×
507
                        this->tc_cursor.y,
×
508
                    };
509
                    auto tok_end = input_point{
510
                        (int) sf.byte_to_column_index(tok_sf.sf_end)
×
511
                            - this->tc_left,
×
512
                        this->tc_cursor.y,
×
513
                    };
514

515
                    log_debug("st %d:%d", tok_start.x, tok_end.x);
×
516
                    this->tc_drag_selection = std::nullopt;
×
517
                    this->tc_selection
518
                        = selected_range::from_mouse(tok_start, tok_end);
×
519
                    this->set_needs_update();
×
520
                }
521
            }
522
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_PRESSED) {
×
523
            this->tc_selection = std::nullopt;
×
524
            this->tc_cursor_anchor = inner_press_point;
×
525
            this->tc_drag_selection = selected_range::from_mouse(
×
526
                this->tc_cursor_anchor, inner_point);
×
527
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_DRAGGED) {
×
528
            this->tc_drag_selection = selected_range::from_mouse(
×
529
                this->tc_cursor_anchor, inner_point);
×
530
            this->set_needs_update();
×
531
        } else if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
×
532
            this->tc_drag_selection = std::nullopt;
×
533
            if (inner_press_point == inner_point) {
×
534
                this->tc_selection = std::nullopt;
×
535
            } else {
536
                this->tc_selection = selected_range::from_mouse(
×
537
                    this->tc_cursor_anchor, inner_point);
×
538
            }
539
            this->set_needs_update();
×
540
        }
541
        this->ensure_cursor_visible();
×
542
    }
543

544
    return true;
×
545
}
546

547
bool
548
textinput_curses::handle_help_key(const ncinput& ch)
×
549
{
550
    switch (ch.id) {
×
551
        case ' ':
×
552
        case 'b':
553
        case 'j':
554
        case 'k':
555
        case 'g':
556
        case 'G':
557
        case NCKEY_HOME:
558
        case NCKEY_END:
559
        case NCKEY_UP:
560
        case NCKEY_DOWN:
561
        case NCKEY_PGUP:
562
        case NCKEY_PGDOWN: {
563
            log_debug("passing key press to help view");
×
564
            return this->tc_help_view.handle_key(ch);
×
565
        }
566
        default: {
×
567
            log_debug("switching back to editing from help");
×
568
            this->tc_mode = mode_t::editing;
×
569
            this->tc_help_view.set_visible(false);
×
570
            if (this->tc_on_change) {
×
571
                this->tc_on_change(*this);
×
572
            }
573
            this->set_needs_update();
×
574
            return true;
×
575
        }
576
    }
577
}
578

579
bool
580
textinput_curses::handle_search_key(const ncinput& ch)
×
581
{
582
    if (ncinput_ctrl_p(&ch)) {
×
583
        switch (ch.id) {
×
584
            case 'a':
×
585
            case 'A':
586
            case 'e':
587
            case 'E': {
588
                this->tc_mode = mode_t::editing;
×
589
                return this->handle_key(ch);
×
590
            }
591
            case 's':
×
592
            case 'S': {
593
                if (!this->tc_search.empty()) {
×
594
                    this->tc_search_start_point = this->tc_cursor;
×
595
                    this->move_cursor_to_next_search_hit();
×
596
                }
597
                return true;
×
598
            }
599
            case 'r':
×
600
            case 'R': {
601
                if (!this->tc_search.empty()) {
×
602
                    this->tc_search_start_point = this->tc_cursor;
×
603
                    this->move_cursor_to_prev_search_hit();
×
604
                }
605
                return true;
×
606
            }
607
        }
608
        return false;
×
609
    }
610

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

660
                if (!this->tc_search_found.has_value()) {
×
661
                    this->tc_search.clear();
×
662
                }
663
                this->tc_search.append(utf8);
×
664
                if (!this->tc_search.empty()) {
×
665
                    auto compile_res = lnav::pcre2pp::code::from(
666
                        lnav::pcre2pp::quote(this->tc_search), PCRE2_CASELESS);
×
667
                    this->tc_search_code = compile_res.unwrap().to_shared();
×
668
                    this->move_cursor_to_next_search_hit();
×
669
                }
670
            }
671
            return true;
×
672
        }
673
    }
674

675
    return false;
676
}
677

678
void
679
textinput_curses::move_cursor_to_next_search_hit()
×
680
{
681
    if (this->tc_search_code == nullptr) {
×
682
        return;
×
683
    }
684

685
    auto x = this->tc_search_start_point.x;
×
686
    if (this->tc_search_found && !this->tc_search_found.value()) {
×
687
        this->tc_search_start_point.y = 0;
×
688
    }
689
    this->tc_search_found = false;
×
690
    for (auto y = this->tc_search_start_point.y;
×
691
         y < (ssize_t) this->tc_lines.size();
×
692
         y++)
693
    {
694
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
×
695

696
        const auto& al = this->tc_lines[y];
×
697
        auto byte_x = al.column_to_byte_index(x);
×
698
        auto after_x_sf = al.to_string_fragment().substr(byte_x);
×
699
        auto find_res = this->tc_search_code->capture_from(after_x_sf)
×
700
                            .into(md)
×
701
                            .matches()
×
702
                            .ignore_error();
×
703
        if (find_res) {
×
704
            this->tc_cursor.x
705
                = al.byte_to_column_index(find_res.value().f_all.sf_end);
×
706
            this->tc_cursor.y = y;
×
707
            log_debug(
×
708
                "search found %d:%d", this->tc_cursor.x, this->tc_cursor.y);
709
            this->tc_search_found = true;
×
710
            this->ensure_cursor_visible();
×
711
            break;
×
712
        }
713
        x = 0;
×
714
    }
715
    this->set_needs_update();
×
716
}
717

718
void
719
textinput_curses::move_cursor_to_prev_search_hit()
×
720
{
721
    auto max_x = std::make_optional(this->tc_search_start_point.x);
×
722
    if (this->tc_search_found && !this->tc_search_found.value()) {
×
723
        this->tc_search_start_point.y = this->tc_lines.size() - 1;
×
724
    }
725
    this->tc_search_found = false;
×
726
    for (auto y = this->tc_search_start_point.y; y >= 0; y--) {
×
727
        thread_local auto md = lnav::pcre2pp::match_data::unitialized();
×
728

729
        const auto& al = this->tc_lines[y];
×
730
        auto before_x_sf = al.to_string_fragment();
×
731
        if (max_x) {
×
732
            before_x_sf = before_x_sf.sub_cell_range(0, max_x.value());
×
733
        }
734
        auto find_res = this->tc_search_code->capture_from(before_x_sf)
×
735
                            .into(md)
×
736
                            .matches()
×
737
                            .ignore_error();
×
738
        if (find_res) {
×
739
            auto new_input_point = input_point{
740
                (int) al.byte_to_column_index(find_res.value().f_all.sf_end),
×
741
                y,
742
            };
743
            if (new_input_point != this->tc_cursor) {
×
744
                this->tc_cursor = new_input_point;
×
745
                this->tc_search_found = true;
×
746
                this->ensure_cursor_visible();
×
747
                break;
×
748
            }
749
        }
750
        max_x = std::nullopt;
×
751
    }
752
    this->set_needs_update();
×
753
}
754

755
void
756
textinput_curses::command_indent(indent_mode_t mode)
×
757
{
758
    log_debug("indenting line: %d", this->tc_cursor.y);
×
759

760
    if (this->tc_cursor.y == 0 && !this->tc_prefix.empty()) {
×
761
        return;
×
762
    }
763

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

810
void
811
textinput_curses::command_down(const ncinput& ch)
×
812
{
813
    if (this->tc_popup.is_visible()) {
×
814
        this->tc_popup.handle_key(ch);
×
815
        if (this->tc_on_popup_change) {
×
816
            this->tc_in_popup_change = true;
×
817
            this->tc_on_popup_change(*this);
×
818
            this->tc_in_popup_change = false;
×
819
        }
820
    } else {
821
        ssize_t inner_height = this->tc_lines.size();
×
822
        if (ncinput_shift_p(&ch)) {
×
823
            if (!this->tc_selection) {
×
824
                this->tc_cursor_anchor = this->tc_cursor;
×
825
            }
826
        }
827
        if (this->tc_cursor.y + 1 < inner_height) {
×
828
            this->move_cursor_by({direction_t::down, 1});
×
829
        } else {
830
            this->move_cursor_to({
×
831
                (int) this->tc_lines[this->tc_cursor.y].column_width(),
×
832
                (int) this->tc_lines.size() - 1,
×
833
            });
834
        }
835
        if (ncinput_shift_p(&ch)) {
×
836
            this->tc_selection = selected_range::from_key(
×
837
                this->tc_cursor_anchor, this->tc_cursor);
×
838
        }
839
    }
840
}
841

842
void
843
textinput_curses::command_up(const ncinput& ch)
×
844
{
845
    if (this->tc_popup.is_visible()) {
×
846
        this->tc_popup.handle_key(ch);
×
847
        if (this->tc_on_popup_change) {
×
848
            this->tc_in_popup_change = true;
×
849
            this->tc_on_popup_change(*this);
×
850
            this->tc_in_popup_change = false;
×
851
        }
852
    } else if (this->tc_height == 1) {
×
853
        if (this->tc_on_history_list) {
×
854
            this->tc_on_history_list(*this);
×
855
        }
856
    } else {
857
        if (ncinput_shift_p(&ch)) {
×
858
            log_debug("up shift");
×
859
            if (!this->tc_selection) {
×
860
                this->tc_cursor_anchor = this->tc_cursor;
×
861
            }
862
        }
863
        if (this->tc_cursor.y > 0) {
×
864
            this->move_cursor_by({direction_t::up, 1});
×
865
        } else {
866
            this->move_cursor_to({0, 0});
×
867
        }
868
        if (ncinput_shift_p(&ch)) {
×
869
            this->tc_selection = selected_range::from_key(
×
870
                this->tc_cursor_anchor, this->tc_cursor);
×
871
        }
872
    }
873
}
874

875
void
NEW
876
textinput_curses::kill_word_backward()
×
877
{
NEW
878
    log_debug("cutting to beginning of previous word");
×
NEW
879
    auto al_sf = this->tc_lines[this->tc_cursor.y].to_string_fragment();
×
NEW
880
    auto prev_word_start_opt = al_sf.prev_word(this->tc_cursor.x);
×
NEW
881
    if (!prev_word_start_opt && this->tc_cursor.x > 0) {
×
NEW
882
        prev_word_start_opt = 0;
×
883
    }
NEW
884
    if (prev_word_start_opt) {
×
NEW
885
        if (this->tc_cut_location != this->tc_cursor) {
×
NEW
886
            log_debug("  cursor moved since last cut, clearing clipboard");
×
NEW
887
            this->tc_clipboard.clear();
×
888
        }
NEW
889
        auto prev_word = al_sf.sub_cell_range(prev_word_start_opt.value(),
×
890
                                              this->tc_cursor.x);
NEW
891
        this->tc_clipboard.emplace_front(prev_word.to_string());
×
NEW
892
        this->sync_to_sysclip();
×
NEW
893
        this->tc_selection = selected_range::from_key(
×
NEW
894
            this->tc_cursor.copy_with_x(prev_word_start_opt.value()),
×
NEW
895
            this->tc_cursor);
×
NEW
896
        this->replace_selection(string_fragment{});
×
NEW
897
        this->tc_cut_location = this->tc_cursor;
×
898
    }
899
}
900

901
void
NEW
902
textinput_curses::kill_word_forward()
×
903
{
NEW
904
    log_debug("cutting to end of next word");
×
NEW
905
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
NEW
906
    auto al_sf = al.to_string_fragment();
×
NEW
907
    auto next_word_end_opt = al_sf.next_word(this->tc_cursor.x);
×
NEW
908
    auto end_x = (int) next_word_end_opt.value_or(al.column_width());
×
NEW
909
    if (end_x <= this->tc_cursor.x) {
×
NEW
910
        return;
×
911
    }
NEW
912
    if (this->tc_cut_location != this->tc_cursor) {
×
NEW
913
        log_debug("  cursor moved since last cut, clearing clipboard");
×
NEW
914
        this->tc_clipboard.clear();
×
915
    }
NEW
916
    auto killed = al_sf.sub_cell_range(this->tc_cursor.x, end_x);
×
NEW
917
    this->tc_clipboard.emplace_back(killed.to_string());
×
NEW
918
    this->sync_to_sysclip();
×
NEW
919
    this->tc_selection = selected_range::from_key(
×
NEW
920
        this->tc_cursor, this->tc_cursor.copy_with_x(end_x));
×
NEW
921
    this->replace_selection(string_fragment{});
×
NEW
922
    this->tc_cut_location = this->tc_cursor;
×
923
}
924

925
void
NEW
926
textinput_curses::change_word_case(uint32_t (*xform)(uint32_t))
×
927
{
NEW
928
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
NEW
929
    auto al_sf = al.to_string_fragment();
×
930
    int range_start;
NEW
931
    if (al_sf.curr_word(this->tc_cursor.x)) {
×
NEW
932
        range_start = this->tc_cursor.x;
×
933
    } else {
NEW
934
        auto next_start_opt = al_sf.next_word(this->tc_cursor.x);
×
NEW
935
        if (!next_start_opt) {
×
NEW
936
            return;
×
937
        }
NEW
938
        range_start = next_start_opt.value();
×
939
    }
940

NEW
941
    auto next_word_end_opt = al_sf.next_word(range_start);
×
NEW
942
    auto end_x = (int) next_word_end_opt.value_or(al.column_width());
×
NEW
943
    if (end_x <= range_start) {
×
NEW
944
        return;
×
945
    }
946
    auto transformed
NEW
947
        = al_sf.sub_cell_range(range_start, end_x).transform_codepoints(xform);
×
NEW
948
    auto start_cursor = this->tc_cursor.copy_with_x(range_start);
×
NEW
949
    auto end_cursor = this->tc_cursor.copy_with_x(end_x);
×
NEW
950
    this->tc_selection = selected_range::from_key(start_cursor, end_cursor);
×
NEW
951
    this->replace_selection(transformed);
×
NEW
952
    this->move_cursor_to(end_cursor);
×
953
}
954

955
void
NEW
956
textinput_curses::capitalize_word()
×
957
{
NEW
958
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
NEW
959
    auto al_sf = al.to_string_fragment();
×
NEW
960
    auto curr_word_opt = al_sf.curr_word(this->tc_cursor.x);
×
961
    int word_start;
NEW
962
    if (curr_word_opt) {
×
NEW
963
        word_start = curr_word_opt.value();
×
964
    } else {
NEW
965
        auto next_start_opt = al_sf.next_word(this->tc_cursor.x);
×
NEW
966
        if (!next_start_opt) {
×
NEW
967
            return;
×
968
        }
NEW
969
        word_start = next_start_opt.value();
×
970
    }
971

NEW
972
    auto next_word_end_opt = al_sf.next_word(word_start);
×
NEW
973
    auto end_x = (int) next_word_end_opt.value_or(al.column_width());
×
NEW
974
    if (end_x <= word_start) {
×
NEW
975
        return;
×
976
    }
NEW
977
    bool saw_letter = false;
×
978
    auto transformed
NEW
979
        = al_sf.sub_cell_range(word_start, end_x)
×
NEW
980
              .transform_codepoints([&saw_letter](uint32_t cp) -> uint32_t {
×
NEW
981
                  if (!uc_is_general_category_withtable(cp,
×
982
                                                        UC_CATEGORY_MASK_L))
983
                  {
NEW
984
                      return cp;
×
985
                  }
NEW
986
                  auto new_cp
×
NEW
987
                      = saw_letter ? uc_tolower(cp) : uc_toupper(cp);
×
NEW
988
                  saw_letter = true;
×
NEW
989
                  return new_cp;
×
NEW
990
              });
×
NEW
991
    auto start_cursor = this->tc_cursor.copy_with_x(word_start);
×
NEW
992
    auto end_cursor = this->tc_cursor.copy_with_x(end_x);
×
NEW
993
    this->tc_selection = selected_range::from_key(start_cursor, end_cursor);
×
NEW
994
    this->replace_selection(transformed);
×
NEW
995
    this->move_cursor_to(end_cursor);
×
996
}
997

998
void
NEW
999
textinput_curses::transpose_chars()
×
1000
{
NEW
1001
    const auto& al = this->tc_lines[this->tc_cursor.y];
×
NEW
1002
    auto col_count = (int) al.column_width();
×
NEW
1003
    if (col_count < 2 || this->tc_cursor.x == 0) {
×
NEW
1004
        return;
×
1005
    }
1006

1007
    int swap_left;
1008
    int swap_right;
1009
    bool advance_cursor;
NEW
1010
    if (this->tc_cursor.x >= col_count) {
×
NEW
1011
        swap_left = col_count - 2;
×
NEW
1012
        swap_right = col_count - 1;
×
NEW
1013
        advance_cursor = false;
×
1014
    } else {
NEW
1015
        swap_left = this->tc_cursor.x - 1;
×
NEW
1016
        swap_right = this->tc_cursor.x;
×
NEW
1017
        advance_cursor = true;
×
1018
    }
1019

NEW
1020
    auto al_sf = al.to_string_fragment();
×
NEW
1021
    auto left_char = al_sf.sub_cell_range(swap_left, swap_right).to_string();
×
1022
    auto right_char
NEW
1023
        = al_sf.sub_cell_range(swap_right, swap_right + 1).to_string();
×
NEW
1024
    this->tc_selection = selected_range::from_key(
×
1025
        this->tc_cursor.copy_with_x(swap_left),
NEW
1026
        this->tc_cursor.copy_with_x(swap_right + 1));
×
NEW
1027
    this->replace_selection(right_char + left_char);
×
NEW
1028
    if (advance_cursor) {
×
NEW
1029
        this->move_cursor_to(this->tc_cursor.copy_with_x(swap_right + 1));
×
1030
    }
1031
}
1032

1033
bool
1034
textinput_curses::handle_key(const ncinput& ch)
4✔
1035
{
1036
    static const auto PREFIX_RE = lnav::pcre2pp::code::from_const(
1037
        R"(^\s*((?:-|\*|1\.|>)(?:\s+\[( |x|X)\])?\s*))");
4✔
1038
    static const auto PREFIX_OR_WS_RE = lnav::pcre2pp::code::from_const(
1039
        R"(^\s*(>\s*|(?:-|\*|1\.)?(?:\s+\[( |x|X)\])?\s+))");
4✔
1040
    thread_local auto md = lnav::pcre2pp::match_data::unitialized();
4✔
1041

1042
    if (this->tc_abort_requested && ch.id != NCKEY_ESC) {
4✔
1043
        this->tc_abort_requested = false;
×
1044
        this->set_needs_update();
×
1045
    }
1046
    if (this->tc_notice) {
4✔
1047
        this->tc_notice = std::nullopt;
×
1048
        switch (ch.id) {
×
1049
            case NCKEY_F01:
×
1050
            case NCKEY_UP:
1051
            case NCKEY_DOWN:
1052
            case NCKEY_LEFT:
1053
            case NCKEY_RIGHT:
1054
                break;
×
1055
            default:
×
1056
                return true;
×
1057
        }
1058
    }
1059
    this->tc_last_tick_after_input = std::nullopt;
4✔
1060
    switch (this->tc_mode) {
4✔
1061
        case mode_t::searching:
×
1062
            return this->handle_search_key(ch);
×
1063
        case mode_t::show_help:
×
1064
            return this->handle_help_key(ch);
×
1065
        case mode_t::editing:
4✔
1066
            break;
4✔
1067
    }
1068

1069
    if (this->tc_mode == mode_t::searching) {
4✔
1070
        return this->handle_search_key(ch);
×
1071
    }
1072

1073
    auto dim = this->get_visible_dimensions();
4✔
1074
    auto inner_height = this->tc_lines.size();
4✔
1075
    auto bottom = inner_height - 1;
4✔
1076
    auto chid = ch.id;
4✔
1077

1078
    if (ch.id == NCKEY_PASTE) {
4✔
1079
        static const auto lf_re = lnav::pcre2pp::code::from_const("\r\n?");
1080
        auto paste_sf = string_fragment::from_c_str(ch.paste_content);
×
1081
        if (!this->tc_selection) {
×
1082
            this->tc_selection = selected_range::from_point(this->tc_cursor);
×
1083
        }
1084
        auto text = lf_re.replace(paste_sf, "\n");
×
1085
        log_debug("applying bracketed paste of size %zu", text.length());
×
1086
        this->replace_selection(text);
×
1087
        return true;
×
1088
    }
1089

1090
    if (ncinput_super_p(&ch)) {
4✔
1091
        switch (chid) {
×
1092
            case 'a': {
×
1093
                this->tc_selection
1094
                    = this->clamp_selection(selected_range::from_mouse(
×
1095
                        input_point::home(), input_point::end()));
×
1096
                this->set_needs_update();
×
1097
                break;
×
1098
            }
1099
            case 'c': {
×
1100
                this->copy_selection();
×
1101
                break;
×
1102
            }
1103
            case 'x': {
×
1104
                this->cut_selection();
×
1105
                break;
×
1106
            }
1107
            case 'z': {
×
1108
                this->undo_last_change();
×
1109
                break;
×
1110
            }
1111
        }
1112
        return true;
×
1113
    }
1114

1115
    if (ncinput_alt_p(&ch)) {
4✔
1116
        switch (chid) {
×
NEW
1117
            case 'b':
×
1118
            case 'B':
1119
            case NCKEY_LEFT: {
1120
                auto& al = this->tc_lines[this->tc_cursor.y];
×
1121
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
1122
                                        .prev_word(this->tc_cursor.x);
×
1123

1124
                this->move_cursor_to(
×
1125
                    this->tc_cursor.copy_with_x(next_col_opt.value_or(0)));
×
1126
                return true;
×
1127
            }
NEW
1128
            case 'f':
×
1129
            case 'F':
1130
            case NCKEY_RIGHT: {
1131
                auto& al = this->tc_lines[this->tc_cursor.y];
×
1132
                auto next_col_opt = string_fragment::from_str(al.al_string)
×
1133
                                        .next_word(this->tc_cursor.x);
×
1134
                this->move_cursor_to(
×
1135
                    this->tc_cursor.copy_with_x(next_col_opt.value_or(
1136
                        this->tc_lines[this->tc_cursor.y].column_width())));
×
1137
                return true;
×
1138
            }
NEW
1139
            case 'c':
×
1140
            case 'C': {
NEW
1141
                this->capitalize_word();
×
NEW
1142
                return true;
×
1143
            }
NEW
1144
            case 'd':
×
1145
            case 'D': {
NEW
1146
                this->kill_word_forward();
×
NEW
1147
                return true;
×
1148
            }
NEW
1149
            case NCKEY_BACKSPACE: {
×
NEW
1150
                this->kill_word_backward();
×
NEW
1151
                return true;
×
1152
            }
NEW
1153
            case 'l':
×
1154
            case 'L': {
NEW
1155
                this->change_word_case(uc_tolower);
×
NEW
1156
                return true;
×
1157
            }
NEW
1158
            case 'u':
×
1159
            case 'U': {
NEW
1160
                this->change_word_case(uc_toupper);
×
NEW
1161
                return true;
×
1162
            }
1163
        }
1164
    }
1165

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

1359
                this->tc_selection = std::nullopt;
×
1360
                this->tc_drag_selection = std::nullopt;
×
1361
                return true;
×
1362
            }
1363
            case '_': {
×
1364
                this->undo_last_change();
×
1365
                return true;
×
1366
            }
1367
            default: {
×
1368
                this->tc_notice = unhandled_input();
×
1369
                this->set_needs_update();
×
1370
                return false;
×
1371
            }
1372
        }
1373
    }
1374

1375
    switch (chid) {
4✔
1376
        case NCKEY_ESC:
×
1377
        case KEY_CTRL(']'): {
1378
            if (this->tc_popup.is_visible()) {
×
1379
                if (this->tc_on_popup_cancel) {
×
1380
                    this->tc_on_popup_cancel(*this);
×
1381
                }
1382
                this->tc_popup_type = popup_type_t::none;
×
1383
                this->tc_popup.set_visible(false);
×
1384
                this->tc_complete_range = std::nullopt;
×
1385
                this->set_needs_update();
×
1386
            } else if (chid != NCKEY_ESC || this->tc_abort_requested) {
×
1387
                this->abort();
×
1388
            } else {
1389
                this->tc_abort_requested = true;
×
1390
                this->set_needs_update();
×
1391
            }
1392

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

1501
                this->command_indent(ncinput_shift_p(&ch)
×
1502
                                         ? indent_mode_t::left
1503
                                         : indent_mode_t::right);
1504
            }
1505
            return true;
×
1506
        }
1507
        case NCKEY_HOME: {
×
1508
            this->move_cursor_to(input_point::home());
×
1509
            return true;
×
1510
        }
1511
        case NCKEY_END: {
×
1512
            this->move_cursor_to(input_point::end());
×
1513
            return true;
×
1514
        }
1515
        case NCKEY_PGUP: {
×
1516
            if (this->tc_cursor.y > 0) {
×
1517
                this->move_cursor_by({direction_t::up, (size_t) dim.dr_height});
×
1518
            }
1519
            return true;
×
1520
        }
1521
        case NCKEY_PGDOWN: {
×
1522
            if (this->tc_cursor.y < (ssize_t) bottom) {
×
1523
                this->move_cursor_by(
×
1524
                    {direction_t::down, (size_t) dim.dr_height});
×
1525
            }
1526
            return true;
×
1527
        }
1528
        case NCKEY_DEL: {
×
1529
            this->tc_selection = selected_range::from_key(
×
1530
                this->tc_cursor,
1531
                this->tc_cursor + movement{direction_t::right, 1});
×
1532
            this->replace_selection(string_fragment{});
×
1533
            break;
×
1534
        }
1535
        case NCKEY_BACKSPACE: {
×
1536
            if (this->tc_lines.size() == 1 && this->tc_lines.front().empty()) {
×
1537
                this->abort();
×
1538
            } else if (!this->tc_selection) {
×
1539
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
1540
                auto line_sf = al.to_string_fragment();
×
1541
                const auto [before, after] = line_sf.split_n(
×
1542
                    line_sf.column_to_byte_index(this->tc_cursor.x));
×
1543
                auto match_opt = PREFIX_RE.capture_from(before)
×
1544
                                     .into(md)
×
1545
                                     .matches()
×
1546
                                     .ignore_error();
×
1547

1548
                if (match_opt && !match_opt->f_all.empty()
×
1549
                    && match_opt->f_all.sf_end == this->tc_cursor.x)
×
1550
                {
1551
                    auto is_comment = al.al_attrs
×
1552
                        | lnav::itertools::find_if([](const string_attr& sa) {
×
1553
                                          return (sa.sa_type == &VC_ROLE)
×
1554
                                              && sa.sa_value.get<role_t>()
×
1555
                                              == role_t::VCR_COMMENT;
×
1556
                                      });
×
1557
                    if (!is_comment && md[1]) {
×
1558
                        this->tc_selection = selected_range::from_key(
×
1559
                            this->tc_cursor.copy_with_x(md[1]->sf_begin),
×
1560
                            this->tc_cursor);
×
1561
                        auto indent = std::string(
1562
                            md[1]->startswith(">") ? 0 : md[1]->length(), ' ');
×
1563

1564
                        this->replace_selection(indent);
×
1565
                        return true;
×
1566
                    }
1567
                } else {
1568
                    auto indent_iter
1569
                        = std::lower_bound(this->tc_doc_meta.m_indents.begin(),
×
1570
                                           this->tc_doc_meta.m_indents.end(),
1571
                                           this->tc_cursor.x);
×
1572
                    if (indent_iter != this->tc_doc_meta.m_indents.end()) {
×
1573
                        if (indent_iter != this->tc_doc_meta.m_indents.begin())
×
1574
                        {
1575
                            auto prev_indent_iter = std::prev(indent_iter);
×
1576
                            this->tc_selection = selected_range::from_key(
×
1577
                                this->tc_cursor.copy_with_x(*prev_indent_iter),
×
1578
                                this->tc_cursor);
×
1579
                        }
1580
                    }
1581
                }
1582
                if (!this->tc_selection) {
×
1583
                    this->tc_selection
1584
                        = selected_range::from_point_and_movement(
×
1585
                            this->tc_cursor, movement{direction_t::left, 1});
×
1586
                }
1587
            }
1588
            this->replace_selection(string_fragment{});
×
1589
            return true;
×
1590
        }
1591
        case NCKEY_UP: {
×
1592
            this->command_up(ch);
×
1593
            return true;
×
1594
        }
1595
        case NCKEY_DOWN: {
×
1596
            this->command_down(ch);
×
1597
            return true;
×
1598
        }
1599
        case NCKEY_LEFT: {
×
1600
            if (ncinput_shift_p(&ch)) {
×
1601
                if (!this->tc_selection) {
×
1602
                    this->tc_cursor_anchor = this->tc_cursor;
×
1603
                }
1604
                this->move_cursor_by({direction_t::left, 1});
×
1605
                this->tc_selection = selected_range::from_key(
×
1606
                    this->tc_cursor_anchor, this->tc_cursor);
×
1607
            } else if (this->tc_selection) {
×
1608
                this->tc_cursor = this->tc_selection->sr_start;
×
1609
                this->tc_selection = std::nullopt;
×
1610
                this->set_needs_update();
×
1611
            } else {
1612
                this->move_cursor_by({direction_t::left, 1});
×
1613
            }
1614
            return true;
×
1615
        }
1616
        case NCKEY_RIGHT: {
×
1617
            if (ncinput_shift_p(&ch)) {
×
1618
                if (!this->tc_selection) {
×
1619
                    this->tc_cursor_anchor = this->tc_cursor;
×
1620
                }
1621
                this->move_cursor_by({direction_t::right, 1});
×
1622
                this->tc_selection = selected_range::from_key(
×
1623
                    this->tc_cursor_anchor, this->tc_cursor);
×
1624
            } else if (this->tc_selection) {
×
1625
                this->tc_cursor = this->tc_selection->sr_end;
×
1626
                this->tc_selection = std::nullopt;
×
1627
                this->set_needs_update();
×
1628
            } else {
1629
                this->move_cursor_by({direction_t::right, 1});
×
1630
            }
1631
            return true;
×
1632
        }
1633
        case NCKEY_F01: {
×
1634
            if (this->tc_on_help) {
×
1635
                this->tc_on_help(*this);
×
1636
            }
1637
            return true;
×
1638
        }
1639
        case ' ': {
×
1640
            if (!this->tc_selection) {
×
1641
                const auto& al = this->tc_lines[this->tc_cursor.y];
×
1642
                const auto sf = al.to_string_fragment();
×
1643
                if (PREFIX_RE.capture_from(sf).into(md).found_p() && md[2]
×
1644
                    && this->tc_cursor.x == md[2]->sf_begin)
×
1645
                {
1646
                    this->tc_selection = selected_range::from_key(
×
1647
                        this->tc_cursor,
1648
                        this->tc_cursor.copy_with_x(this->tc_cursor.x + 1));
×
1649

1650
                    auto repl = (md[2]->front() == ' ') ? "X"_frag : " "_frag;
×
1651
                    this->replace_selection(repl);
×
1652
                    return true;
×
1653
                }
1654

1655
                this->tc_selection
1656
                    = selected_range::from_point(this->tc_cursor);
×
1657
            }
1658
            this->replace_selection(" "_frag);
×
1659
            return true;
×
1660
        }
1661
        default: {
3✔
1662
            if (NCKEY_F00 <= ch.id && ch.id <= NCKEY_F60) {
3✔
1663
                this->tc_notice = unhandled_input();
×
1664
                this->set_needs_update();
×
1665
            } else {
1666
                char utf8[32];
1667
                size_t index = 0;
3✔
1668
                for (const auto eff_ch : ch.eff_text) {
6✔
1669
                    log_debug(" eff %x", eff_ch);
6✔
1670
                    if (eff_ch == 0) {
6✔
1671
                        break;
3✔
1672
                    }
1673
                    ww898::utf::utf8::write(eff_ch,
3✔
1674
                                            [&utf8, &index](const char bits) {
3✔
1675
                                                utf8[index] = bits;
3✔
1676
                                                index += 1;
3✔
1677
                                            });
3✔
1678
                }
1679
                if (index > 0) {
3✔
1680
                    utf8[index] = 0;
3✔
1681

1682
                    if (!this->tc_selection) {
3✔
1683
                        this->tc_selection
1684
                            = selected_range::from_point(this->tc_cursor);
3✔
1685
                    }
1686
                    this->replace_selection(string_fragment::from_c_str(utf8));
3✔
1687
                } else {
1688
                    this->tc_notice = unhandled_input();
×
1689
                    this->set_needs_update();
×
1690
                }
1691
            }
1692
            return true;
3✔
1693
        }
1694
    }
1695

1696
    return false;
×
1697
}
1698

1699
void
1700
textinput_curses::ensure_cursor_visible()
18✔
1701
{
1702
    if (!this->vc_enabled) {
18✔
1703
        return;
15✔
1704
    }
1705

1706
    auto dim = this->get_visible_dimensions();
3✔
1707
    auto orig_top = this->tc_top;
3✔
1708
    auto orig_left = this->tc_left;
3✔
1709
    auto orig_cursor = this->tc_cursor;
3✔
1710
    auto orig_max_cursor_x = this->tc_max_cursor_x;
3✔
1711

1712
    this->clamp_point(this->tc_cursor);
3✔
1713
    if (this->tc_cursor.y < 0) {
3✔
1714
        this->tc_cursor.y = 0;
×
1715
    }
1716
    if (this->tc_cursor.y >= (ssize_t) this->tc_lines.size()) {
3✔
1717
        this->tc_cursor.y = this->tc_lines.size() - 1;
×
1718
    }
1719
    if (this->tc_cursor.x < 0) {
3✔
1720
        this->tc_cursor.x = 0;
×
1721
    }
1722
    if (this->tc_cursor.x
6✔
1723
        >= (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1724
    {
1725
        this->tc_cursor.x = this->tc_lines[this->tc_cursor.y].column_width();
3✔
1726
    }
1727

1728
    if (this->tc_cursor.x <= this->tc_left) {
3✔
1729
        this->tc_left = this->tc_cursor.x;
×
1730
        if (this->tc_left > 0) {
×
1731
            this->tc_left -= 1;
×
1732
        }
1733
    }
1734
    if (this->tc_cursor.x >= this->tc_left + (dim.dr_width - 2)) {
3✔
1735
        this->tc_left = (this->tc_cursor.x - dim.dr_width) + 2;
×
1736
    }
1737
    if (this->tc_top < 0) {
3✔
1738
        this->tc_top = 0;
×
1739
    }
1740
    if (this->tc_top >= this->tc_cursor.y) {
3✔
1741
        this->tc_top = this->tc_cursor.y;
3✔
1742
        if (this->tc_top > 0) {
3✔
1743
            this->tc_top -= 1;
×
1744
        }
1745
    }
1746
    if (this->tc_height > 1
3✔
1747
        && this->tc_cursor.y + 1 >= this->tc_top + dim.dr_height)
×
1748
    {
1749
        this->tc_top = (this->tc_cursor.y + 1 - dim.dr_height) + 1;
×
1750
    }
1751
    if (this->tc_top + dim.dr_height > (ssize_t) this->tc_lines.size()) {
3✔
1752
        if ((ssize_t) this->tc_lines.size() > dim.dr_height) {
×
1753
            this->tc_top = this->tc_lines.size() - dim.dr_height + 1;
×
1754
        } else {
1755
            this->tc_top = 0;
×
1756
        }
1757
    }
1758
    if (!this->tc_in_popup_change && this->tc_popup.is_visible()
3✔
1759
        && this->tc_complete_range
×
1760
        && !this->tc_complete_range->contains(this->tc_cursor))
6✔
1761
    {
1762
        this->tc_popup.set_visible(false);
×
1763
        this->tc_complete_range = std::nullopt;
×
1764
    }
1765

1766
    if (this->tc_cursor.x
6✔
1767
        == (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
3✔
1768
    {
1769
        if (this->tc_cursor.x >= this->tc_max_cursor_x) {
3✔
1770
            this->tc_max_cursor_x = this->tc_cursor.x;
3✔
1771
        }
1772
    } else {
1773
        this->tc_max_cursor_x = this->tc_cursor.x;
×
1774
    }
1775

1776
    if (orig_top != this->tc_top || orig_left != this->tc_left
3✔
1777
        || orig_cursor != this->tc_cursor
3✔
1778
        || orig_max_cursor_x != this->tc_max_cursor_x)
6✔
1779
    {
1780
        this->set_needs_update();
3✔
1781
    }
1782
}
1783

1784
void
1785
textinput_curses::apply_highlights()
3✔
1786
{
1787
    if (this->tc_text_format == text_format_t::TF_LNAV_SCRIPT) {
3✔
1788
        return;
×
1789
    }
1790

1791
    for (auto& line : this->tc_lines) {
6✔
1792
        for (const auto& hl_pair : this->tc_highlights) {
3✔
1793
            const auto& hl = hl_pair.second;
×
1794

1795
            if (!hl.applies_to_format(this->tc_text_format)) {
×
1796
                continue;
×
1797
            }
1798
            hl.annotate(line, line_range{0, -1});
×
1799
        }
1800
    }
1801
}
1802

1803
std::string
1804
textinput_curses::replace_selection_no_change(string_fragment sf)
3✔
1805
{
1806
    if (!this->tc_selection) {
3✔
1807
        return "";
×
1808
    }
1809

1810
    std::optional<int> del_max;
3✔
1811
    auto full_first_line = false;
3✔
1812
    std::string retval;
3✔
1813

1814
    auto range = std::exchange(this->tc_selection, std::nullopt).value();
3✔
1815
    this->tc_cursor.y = range.sr_start.y;
3✔
1816
    for (auto curr_line = range.sr_start.y;
6✔
1817
         curr_line <= range.sr_end.y && curr_line < this->tc_lines.size();
6✔
1818
         ++curr_line)
1819
    {
1820
        auto sel_range = range.range_for_line(curr_line);
3✔
1821

1822
        if (!sel_range) {
3✔
1823
            continue;
×
1824
        }
1825

1826
        log_debug("sel_range y=%d [%d:%d)",
3✔
1827
                  curr_line,
1828
                  sel_range->lr_start,
1829
                  sel_range->lr_end);
1830
        if (sel_range->lr_start < 0) {
3✔
1831
            if (curr_line > 0) {
×
1832
                log_debug("append %d to %d", curr_line, curr_line - 1);
×
1833
                this->tc_cursor.x
1834
                    = this->tc_lines[curr_line - 1].column_width();
×
1835
                this->tc_cursor.y = curr_line - 1;
×
1836
                this->tc_lines[curr_line - 1].append(this->tc_lines[curr_line]);
×
1837
                retval.push_back('\n');
×
1838
                del_max = curr_line;
×
1839
                full_first_line = true;
×
1840
            }
1841
        } else if (sel_range->lr_start
3✔
1842
                       == (ssize_t) this->tc_lines[curr_line].column_width()
3✔
1843
                   && sel_range->lr_end != -1
3✔
1844
                   && sel_range->lr_start < sel_range->lr_end)
6✔
1845
        {
1846
            // Del deleting line feed
1847
            if (curr_line + 1 < (ssize_t) this->tc_lines.size()) {
×
1848
                this->tc_lines[curr_line].append(this->tc_lines[curr_line + 1]);
×
1849
                retval.push_back('\n');
×
1850
                del_max = curr_line + 1;
×
1851
            }
1852
        } else if (sel_range->lr_start == 0 && sel_range->lr_end == -1) {
3✔
1853
            log_debug("delete full line");
×
1854
            retval.append(this->tc_lines[curr_line].al_string);
×
1855
            retval.push_back('\n');
×
1856
            del_max = curr_line;
×
1857
            if (curr_line == range.sr_start.y) {
×
1858
                log_debug("full first");
×
1859
                full_first_line = true;
×
1860
            }
1861
        } else {
1862
            log_debug("partial line change");
3✔
1863
            auto& al = this->tc_lines[curr_line];
3✔
1864
            auto start = al.column_to_byte_index(sel_range->lr_start);
3✔
1865
            auto end = sel_range->lr_end == -1
3✔
1866
                ? al.al_string.length()
3✔
1867
                : al.column_to_byte_index(sel_range->lr_end);
3✔
1868

1869
            retval.append(al.al_string.substr(start, end - start));
3✔
1870
            if (sel_range->lr_end == -1) {
3✔
1871
                retval.push_back('\n');
×
1872
            }
1873
            al.erase(start, end - start);
3✔
1874
            if (full_first_line || curr_line == range.sr_start.y) {
3✔
1875
                al.insert(start, sf.to_string());
3✔
1876
                this->tc_cursor.x = sel_range->lr_start;
3✔
1877
            }
1878
            if (!full_first_line && sel_range->lr_start == 0
3✔
1879
                && range.sr_start.y < curr_line && curr_line == range.sr_end.y)
6✔
1880
            {
1881
                del_max = curr_line;
×
1882
                this->tc_lines[range.sr_start.y].append(al);
×
1883
            }
1884
        }
1885
    }
1886

1887
    if (del_max) {
3✔
1888
        log_debug("deleting lines [%d+%d:%d)",
×
1889
                  range.sr_start.y,
1890
                  (full_first_line ? 0 : 1),
1891
                  del_max.value() + 1);
1892
        this->tc_lines.erase(this->tc_lines.begin() + range.sr_start.y
×
1893
                                 + (full_first_line ? 0 : 1),
×
1894
                             this->tc_lines.begin() + del_max.value() + 1);
×
1895
    }
1896

1897
    const auto repl_last_line
1898
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'});
3✔
1899
    log_debug(
3✔
1900
        "last line '%.*s'", repl_last_line.length(), repl_last_line.data());
1901
    const auto repl_cols
1902
        = sf.find_left_boundary(sf.length(), string_fragment::tag1{'\n'})
3✔
1903
              .column_width();
3✔
1904
    const auto repl_lines = sf.count('\n');
3✔
1905
    log_debug("repl_cols => %zu", repl_cols);
3✔
1906
    if (repl_lines > 0) {
3✔
1907
        this->tc_cursor.x = repl_cols;
×
1908
    } else {
1909
        this->tc_cursor.x += repl_cols;
3✔
1910
    }
1911
    this->tc_cursor.y += repl_lines;
3✔
1912

1913
    this->tc_drag_selection = std::nullopt;
3✔
1914
    if (retval == sf) {
3✔
1915
        if (!sf.empty()) {
×
1916
            this->content_to_lines(this->get_content(),
×
1917
                                   this->get_cursor_offset());
1918
        }
1919
    } else {
1920
        this->update_lines();
3✔
1921
    }
1922

1923
    ensure(!this->tc_lines.empty());
3✔
1924

1925
    return retval;
3✔
1926
}
3✔
1927

1928
void
1929
textinput_curses::replace_selection(string_fragment sf)
3✔
1930
{
1931
    static constexpr uint32_t mask
1932
        = UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N | UC_CATEGORY_MASK_Pc;
1933

1934
    if (!this->tc_selection) {
3✔
1935
        return;
×
1936
    }
1937
    auto range = this->tc_selection.value();
3✔
1938
    auto change_pos = this->tc_change_log.size();
3✔
1939
    auto old_text = this->replace_selection_no_change(sf);
3✔
1940
    if (old_text == sf) {
3✔
1941
        log_trace("no-op replacement");
×
1942
    } else {
1943
        auto is_wordbreak = !sf.empty()
3✔
1944
            && !uc_is_general_category_withtable(sf.front_codepoint(), mask);
3✔
1945
        log_debug("repl sel [%d:%d) - cursor [%d:%d)",
3✔
1946
                  range.sr_start.x,
1947
                  range.sr_start.y,
1948
                  this->tc_cursor.x,
1949
                  this->tc_cursor.y);
1950
        if (this->tc_change_log.empty()
3✔
1951
            || this->tc_change_log.back().ce_range.sr_end != range.sr_start
2✔
1952
            || is_wordbreak)
5✔
1953
        {
1954
            auto redo_range = selected_range::from_key(
1✔
1955
                range.sr_start.x < 0 ? this->tc_cursor : range.sr_start,
1✔
1956
                this->tc_cursor);
1957
            log_debug("  redo range [%d:%d] - [%d:%d]",
1✔
1958
                      redo_range.sr_start.x,
1959
                      redo_range.sr_start.y,
1960
                      redo_range.sr_end.x,
1961
                      redo_range.sr_end.y);
1962
            if (change_pos < this->tc_change_log.size()) {
1✔
1963
                // XXX an on_change handler can run and do its own replacement
1964
                // before we get a change to add or entry
1965
                log_debug("inserting change log at %zu", change_pos);
×
1966
                this->tc_change_log.insert(
×
1967
                    std::next(this->tc_change_log.begin(), change_pos),
×
1968
                    change_entry{redo_range, old_text});
×
1969
            } else {
1970
                this->tc_change_log.emplace_back(redo_range, old_text);
1✔
1971
            }
1972
        } else {
1973
            auto& last_range = this->tc_change_log.back().ce_range;
2✔
1974
            last_range.sr_end = this->tc_cursor;
2✔
1975
            log_debug("extending undo range [%d:%d] - [%d:%d]",
2✔
1976
                      last_range.sr_start.x,
1977
                      last_range.sr_start.y,
1978
                      last_range.sr_end.x,
1979
                      last_range.sr_end.y);
1980
        }
1981
    }
1982
}
3✔
1983

1984
void
1985
textinput_curses::move_cursor_by(movement move)
×
1986
{
1987
    auto cursor_y_offset = this->tc_cursor.y - this->tc_top;
×
1988
    this->tc_cursor += move;
×
1989
    if (move.hm_dir == direction_t::up || move.hm_dir == direction_t::down) {
×
1990
        if (move.hm_amount > 1) {
×
1991
            this->tc_top = this->tc_cursor.y - cursor_y_offset;
×
1992
            this->set_needs_update();
×
1993
        }
1994
        this->tc_cursor.x = this->tc_max_cursor_x;
×
1995
    }
1996
    if (this->tc_cursor.x < 0) {
×
1997
        if (this->tc_cursor.y > 0) {
×
1998
            this->tc_cursor.y -= 1;
×
1999
            this->tc_cursor.x
2000
                = this->tc_lines[this->tc_cursor.y].column_width();
×
2001
        } else {
2002
            this->tc_cursor.x = 0;
×
2003
        }
2004
    }
2005
    if (move.hm_dir == direction_t::right
×
2006
        && this->tc_cursor.x
×
2007
            > (ssize_t) this->tc_lines[this->tc_cursor.y].column_width())
×
2008
    {
2009
        if (this->tc_cursor.y + 1 < (ssize_t) this->tc_lines.size()) {
×
2010
            this->tc_cursor.x = 0;
×
2011
            this->tc_cursor.y += 1;
×
2012
            this->tc_max_cursor_x = 0;
×
2013
        }
2014
    }
2015
    this->clamp_point(this->tc_cursor);
×
2016
    if (this->tc_drag_selection) {
×
2017
        this->tc_drag_selection = std::nullopt;
×
2018
        this->set_needs_update();
×
2019
    }
2020
    if (this->tc_selection) {
×
2021
        this->tc_selection = std::nullopt;
×
2022
        this->set_needs_update();
×
2023
    }
2024
    this->ensure_cursor_visible();
×
2025
}
2026

2027
void
2028
textinput_curses::move_cursor_to(input_point ip)
1✔
2029
{
2030
    this->tc_cursor = ip;
1✔
2031
    if (this->tc_drag_selection) {
1✔
2032
        this->tc_drag_selection = std::nullopt;
×
2033
        this->set_needs_update();
×
2034
    }
2035
    if (this->tc_selection) {
1✔
2036
        this->tc_selection = std::nullopt;
×
2037
        this->set_needs_update();
×
2038
    }
2039
    this->ensure_cursor_visible();
1✔
2040
}
1✔
2041

2042
void
2043
textinput_curses::update_lines()
3✔
2044
{
2045
    const auto x = this->get_cursor_offset();
3✔
2046
    this->content_to_lines(this->get_content(), x);
3✔
2047
    this->set_needs_update();
3✔
2048
    this->ensure_cursor_visible();
3✔
2049

2050
    this->tc_marks.clear();
3✔
2051
    if (this->tc_in_popup_change) {
3✔
2052
        log_trace("in popup change, skipping");
×
2053
    } else {
2054
        this->tc_popup.set_visible(false);
3✔
2055
        this->tc_complete_range = std::nullopt;
3✔
2056
        if (this->tc_on_change) {
3✔
2057
            this->tc_on_change(*this);
3✔
2058
        }
2059
        if (!this->tc_popup.is_visible()) {
3✔
2060
            this->tc_popup_type = popup_type_t::none;
3✔
2061
        }
2062
    }
2063

2064
    ensure(!this->tc_lines.empty());
3✔
2065
}
3✔
2066

2067
textinput_curses::dimension_result
2068
textinput_curses::get_visible_dimensions() const
23✔
2069
{
2070
    dimension_result retval;
23✔
2071

2072
    ncplane_dim_yx(
23✔
2073
        this->tc_window, &retval.dr_full_height, &retval.dr_full_width);
23✔
2074

2075
    if (this->vc_y < (ssize_t) retval.dr_full_height) {
23✔
2076
        retval.dr_height = std::min((int) retval.dr_full_height - this->vc_y,
23✔
2077
                                    this->tc_height);
23✔
2078
    }
2079
    if (this->vc_x < (ssize_t) retval.dr_full_width) {
23✔
2080
        retval.dr_width = std::min((long) retval.dr_full_width - this->vc_x,
23✔
2081
                                   this->vc_width);
23✔
2082
    }
2083
    return retval;
23✔
2084
}
2085

2086
std::string
2087
textinput_curses::get_content(bool trim) const
7✔
2088
{
2089
    auto need_lf = false;
7✔
2090
    std::string retval;
7✔
2091

2092
    for (const auto& al : this->tc_lines) {
14✔
2093
        const auto& line = al.al_string;
7✔
2094
        auto line_sf = string_fragment::from_str(line);
7✔
2095
        if (trim) {
7✔
2096
            line_sf = line_sf.rtrim(" ");
×
2097
        }
2098
        if (need_lf) {
7✔
2099
            retval.push_back('\n');
×
2100
        }
2101
        retval += line_sf;
7✔
2102
        need_lf = true;
7✔
2103
    }
2104
    return retval;
7✔
2105
}
×
2106

2107
void
2108
textinput_curses::focus()
3✔
2109
{
2110
    if (!this->vc_enabled) {
3✔
2111
        this->vc_enabled = true;
1✔
2112
        if (this->tc_on_focus) {
1✔
2113
            this->tc_on_focus(*this);
×
2114
        }
2115
        this->set_needs_update();
1✔
2116
    }
2117

2118
    if (this->tc_mode == mode_t::show_help
6✔
2119
        || (this->tc_height && this->tc_notice)
3✔
2120
        || (this->tc_selection
6✔
2121
            && this->tc_selection->contains_exclusive(this->tc_cursor)))
3✔
2122
    {
2123
        notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
×
2124
        return;
×
2125
    }
2126
    auto term_x = this->vc_x + this->tc_cursor.x - this->tc_left;
3✔
2127
    if (this->tc_cursor.y == 0) {
3✔
2128
        term_x += this->tc_prefix.column_width();
2✔
2129
    }
2130
    notcurses_cursor_enable(ncplane_notcurses(this->tc_window),
3✔
2131
                            this->vc_y + this->tc_cursor.y - this->tc_top,
3✔
2132
                            term_x);
2133
}
2134

2135
void
2136
textinput_curses::blur()
1✔
2137
{
2138
    this->tc_popup_type = popup_type_t::none;
1✔
2139
    this->tc_popup.set_visible(false);
1✔
2140
    this->vc_enabled = false;
1✔
2141
    if (this->tc_on_blur) {
1✔
2142
        this->tc_on_blur(*this);
1✔
2143
    }
2144

2145
    notcurses_cursor_disable(ncplane_notcurses(this->tc_window));
1✔
2146
    this->set_needs_update();
1✔
2147
}
1✔
2148

2149
void
2150
textinput_curses::abort()
×
2151
{
2152
    this->blur();
×
2153
    this->tc_selection = std::nullopt;
×
2154
    this->tc_drag_selection = std::nullopt;
×
2155
    if (this->tc_on_abort) {
×
2156
        this->tc_on_abort(*this);
×
2157
    }
2158
}
2159

2160
void
2161
textinput_curses::sync_to_sysclip() const
×
2162
{
2163
    auto clip_open_res = sysclip::open(sysclip::type_t::GENERAL);
×
2164

2165
    if (clip_open_res.isOk()) {
×
2166
        auto clip_file = clip_open_res.unwrap();
×
2167
        fmt::print(clip_file.in(),
×
2168
                   FMT_STRING("{}"),
×
2169
                   fmt::join(this->tc_clipboard, ""));
×
2170
    } else {
×
2171
        auto err_msg = clip_open_res.unwrapErr();
×
2172
        log_error("unable to open clipboard: %s", err_msg.c_str());
×
2173
    }
2174
}
2175

2176
bool
2177
textinput_curses::do_update()
25✔
2178
{
2179
    static auto& vc = view_colors::singleton();
25✔
2180
    auto retval = false;
25✔
2181

2182
    if (!this->is_visible()) {
25✔
2183
        return retval;
1✔
2184
    }
2185

2186
    auto popup_height = this->tc_popup.get_height();
24✔
2187
    auto rel_y = (this->tc_popup_type == popup_type_t::history
24✔
2188
                      ? 0
24✔
2189
                      : this->tc_cursor.y - this->tc_top)
24✔
2190
        - popup_height;
24✔
2191
    if (this->vc_y + rel_y < 0) {
24✔
2192
        rel_y = this->tc_cursor.y - this->tc_top + popup_height + 1;
×
2193
    }
2194
    this->tc_popup.set_y(this->vc_y + rel_y);
24✔
2195

2196
    if (!this->vc_needs_update) {
24✔
2197
        return view_curses::do_update();
8✔
2198
    }
2199

2200
    auto dim = this->get_visible_dimensions();
16✔
2201
    if (!this->vc_enabled) {
16✔
2202
        ncplane_erase_region(
15✔
2203
            this->tc_window, this->vc_y, this->vc_x, 1, dim.dr_width);
2204
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
15✔
2205
        mvwattrline(this->tc_window,
×
2206
                    this->vc_y,
2207
                    this->vc_x,
2208
                    this->tc_inactive_value,
15✔
2209
                    lr);
2210

2211
        if (!this->tc_alt_value.empty()
15✔
2212
            && (ssize_t) (this->tc_inactive_value.column_width() + 3
19✔
2213
                          + this->tc_alt_value.column_width())
4✔
2214
                < dim.dr_width)
4✔
2215
        {
2216
            auto alt_x = dim.dr_width - this->tc_alt_value.column_width();
4✔
2217
            auto lr = line_range{0, (int) this->tc_alt_value.column_width()};
4✔
2218
            mvwattrline(
4✔
2219
                this->tc_window, this->vc_y, alt_x, this->tc_alt_value, lr);
4✔
2220
        }
2221

2222
        this->vc_needs_update = false;
15✔
2223
        return true;
15✔
2224
    }
2225

2226
    if (this->tc_mode == mode_t::show_help) {
1✔
2227
        this->tc_help_view.set_window(this->tc_window);
×
2228
        this->tc_help_view.set_x(this->vc_x);
×
2229
        this->tc_help_view.set_y(this->vc_y);
×
2230
        this->tc_help_view.set_width(this->vc_width);
×
2231
        this->tc_help_view.set_height(vis_line_t(this->tc_height));
×
2232
        this->tc_help_view.set_visible(true);
×
2233
        return view_curses::do_update();
×
2234
    }
2235

2236
    retval = true;
1✔
2237
    ssize_t row_count = this->tc_lines.size();
1✔
2238
    auto y = this->vc_y;
1✔
2239
    auto y_max = this->vc_y + dim.dr_height;
1✔
2240
    if (row_count == 1 && this->tc_lines[0].empty()
1✔
2241
        && !this->tc_suggestion.empty())
2✔
2242
    {
2243
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
2244
        auto al = attr_line_t(this->tc_suggestion)
×
2245
                      .with_attr_for_all(VC_ROLE.value(role_t::VCR_SUGGESTION));
×
2246
        al.insert(0, this->tc_prefix);
×
2247
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
×
2248
        mvwattrline(this->tc_window, y, this->vc_x, al, lr);
×
2249
        row_count -= 1;
×
2250
        y += 1;
×
2251
    }
2252
    auto abort_msg_shown = false;
1✔
2253
    for (auto curr_line = this->tc_top; curr_line < row_count && y < y_max;
2✔
2254
         curr_line++, y++)
1✔
2255
    {
2256
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
1✔
2257
        auto lr = line_range{this->tc_left, this->tc_left + dim.dr_width};
1✔
2258
        auto al = this->tc_lines[curr_line];
1✔
2259
        if (this->tc_drag_selection) {
1✔
2260
            auto sel_lr = this->tc_drag_selection->range_for_line(curr_line);
×
2261
            if (sel_lr) {
×
2262
                al.al_attrs.emplace_back(
×
2263
                    sel_lr.value(), VC_ROLE.value(role_t::VCR_SELECTED_TEXT));
×
2264
            }
2265
        } else if (this->tc_selection) {
1✔
2266
            auto sel_lr = this->tc_selection->range_for_line(curr_line);
×
2267
            if (sel_lr) {
×
2268
                al.al_attrs.emplace_back(
×
2269
                    sel_lr.value(), VC_STYLE.value(text_attrs::with_reverse()));
×
2270
            }
2271
        }
2272
        if (this->tc_mode == mode_t::searching
2✔
2273
            && this->tc_search_found.value_or(false))
1✔
2274
        {
2275
            this->tc_search_code->capture_from(al.al_string)
×
2276
                .for_each([&al](lnav::pcre2pp::match_data& md) {
×
2277
                    al.al_attrs.emplace_back(
×
2278
                        line_range{
×
2279
                            md[0]->sf_begin,
×
2280
                            md[0]->sf_end,
×
2281
                        },
2282
                        VC_ROLE.value(role_t::VCR_SEARCH));
×
2283
                });
×
2284
        }
2285
        if (!this->tc_suggestion.empty() && !this->tc_popup.is_visible()
1✔
2286
            && curr_line == this->tc_cursor.y
×
2287
            && this->tc_cursor.x == (ssize_t) al.column_width())
1✔
2288
        {
2289
            al.append(this->tc_suggestion,
×
2290
                      VC_ROLE.value(role_t::VCR_SUGGESTION));
×
2291
        }
2292
        if (curr_line == 0) {
1✔
2293
            al.insert(0, this->tc_prefix);
1✔
2294
        }
2295
        mvwattrline(this->tc_window, y, this->vc_x, al, lr);
1✔
2296

2297
        if (!abort_msg_shown && this->tc_abort_requested) {
1✔
2298
            static auto REQ_MSG
2299
                = attr_line_t("  Press ")
×
2300
                      .append("Esc"_hotkey)
×
2301
                      .append(" to abort  ")
×
2302
                      .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2303
            auto msg_lr = line_range{0, 0 + dim.dr_width};
×
2304
            mvwattrline(
×
2305
                this->tc_window,
2306
                y,
2307
                this->vc_x + dim.dr_width - REQ_MSG.utf8_length_or_length(),
×
2308
                REQ_MSG,
2309
                msg_lr);
2310
            abort_msg_shown = true;
×
2311
        }
2312
    }
1✔
2313
    for (; y < y_max; y++) {
1✔
2314
        static constexpr auto EMPTY_LR = line_range::empty_at(0);
2315

2316
        auto al = attr_line_t();
×
2317
        ncplane_erase_region(this->tc_window, y, this->vc_x, 1, dim.dr_width);
×
2318
        mvwattrline(
×
2319
            this->tc_window, y, this->vc_x, al, EMPTY_LR, role_t::VCR_ALT_ROW);
2320
    }
2321
    if (this->tc_notice) {
1✔
2322
        auto notice_lines = this->tc_notice.value();
×
2323
        auto avail_height = std::min(dim.dr_height, (int) notice_lines.size());
×
2324
        auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2325

2326
        for (auto& al : notice_lines) {
×
2327
            auto lr = line_range{0, dim.dr_width};
×
2328
            mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
2329
            if (notice_y >= y_max) {
×
2330
                break;
×
2331
            }
2332
        }
2333
    } else if (this->tc_mode == mode_t::searching) {
1✔
2334
        auto search_prompt = attr_line_t(" ");
×
2335
        if (this->tc_search.empty() || this->tc_search_found.has_value()) {
×
2336
            search_prompt.append(this->tc_search)
×
2337
                .append(" ", VC_ROLE.value(role_t::VCR_CURSOR_LINE));
×
2338
        } else {
2339
            search_prompt.append(this->tc_search,
×
2340
                                 VC_ROLE.value(role_t::VCR_SEARCH));
×
2341
        }
2342
        if (this->tc_search_found && this->tc_search_found.value()) {
×
2343
            search_prompt.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2344
        }
2345
        search_prompt.insert(0, " Search: "_status_subtitle);
×
2346
        if (this->tc_search_found && !this->tc_search_found.value()) {
×
2347
            search_prompt.with_attr_for_all(
×
2348
                VC_ROLE.value(role_t::VCR_ALERT_STATUS));
×
2349
        }
2350
        auto lr = line_range{0, dim.dr_width};
×
2351
        mvwattrline(this->tc_window,
×
2352
                    this->vc_y + dim.dr_height - 1,
×
2353
                    this->vc_x,
2354
                    search_prompt,
2355
                    lr);
2356
    } else if (this->tc_height > 1) {
1✔
2357
        auto mark_iter = this->tc_marks.find(this->tc_cursor);
×
2358

2359
        if (mark_iter != this->tc_marks.end()) {
×
2360
            auto mark_lines = mark_iter->second.to_attr_line().split_lines();
×
2361
            auto avail_height
2362
                = std::min(dim.dr_height, (int) mark_lines.size());
×
2363
            auto notice_y = this->vc_y + dim.dr_height - avail_height;
×
2364
            for (auto& al : mark_lines) {
×
2365
                al.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
2366
                auto lr = line_range{0, dim.dr_width};
×
2367
                mvwattrline(this->tc_window, notice_y++, this->vc_x, al, lr);
×
2368
                if (notice_y >= y_max) {
×
2369
                    break;
×
2370
                }
2371
            }
2372
        }
2373
    }
2374

2375
    if (this->tc_height > 1) {
1✔
2376
        double progress = 1.0;
×
2377
        double coverage = 1.0;
×
2378

2379
        if (row_count > 0) {
×
2380
            progress = (double) this->tc_top / (double) row_count;
×
2381
            coverage = (double) dim.dr_height / (double) row_count;
×
2382
        }
2383

2384
        auto scroll_top = (int) (progress * (double) dim.dr_height);
×
2385
        auto scroll_bottom = scroll_top
2386
            + std::min(dim.dr_height,
×
2387
                       (int) (coverage * (double) dim.dr_height));
×
2388

2389
        for (auto y = this->vc_y; y < y_max; y++) {
×
2390
            auto role = this->vc_default_role;
×
2391
            auto bar_role = role_t::VCR_SCROLLBAR;
×
2392
            auto ch = NCACS_VLINE;
×
2393
            if (y >= this->vc_y + scroll_top && y <= this->vc_y + scroll_bottom)
×
2394
            {
2395
                role = bar_role;
×
2396
            }
2397
            auto attrs = vc.attrs_for_role(role);
×
2398
            ncplane_putstr_yx(
×
2399
                this->tc_window, y, this->vc_x + dim.dr_width - 1, ch);
×
2400
            ncplane_set_cell_yx(this->tc_window,
×
2401
                                y,
2402
                                this->vc_x + dim.dr_width - 1,
×
2403
                                attrs.ta_attrs | NCSTYLE_ALTCHARSET,
×
2404
                                view_colors::to_channels(attrs));
2405
        }
2406
    }
2407

2408
    return view_curses::do_update() || retval;
1✔
2409
}
2410

2411
void
2412
textinput_curses::open_popup_for_completion(
×
2413
    line_range crange, std::vector<attr_line_t> possibilities)
2414
{
2415
    if (possibilities.empty()) {
×
2416
        this->tc_popup_type = popup_type_t::none;
×
2417
        return;
×
2418
    }
2419

2420
    this->tc_popup_type = popup_type_t::completion;
×
2421
    auto dim = this->get_visible_dimensions();
×
2422
    auto max_width = possibilities
2423
        | lnav::itertools::map(&attr_line_t::column_width)
×
2424
        | lnav::itertools::max();
×
2425

2426
    auto full_width = std::min((int) max_width.value_or(1) + 3, dim.dr_width);
×
2427
    auto new_sel = 0_vl;
×
2428
    auto popup_height = vis_line_t(
2429
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
2430
    ssize_t rel_x = crange.lr_start;
×
2431
    if (this->tc_cursor.y == 0) {
×
2432
        rel_x += this->tc_prefix.column_width();
×
2433
    }
2434
    if (rel_x + full_width > dim.dr_width) {
×
2435
        rel_x = dim.dr_width - full_width;
×
2436
    }
2437
    if (this->vc_x + rel_x > 0) {
×
2438
        rel_x -= 1;  // XXX for border
×
2439
    }
2440
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2441
    if (this->vc_y + rel_y < 0) {
×
2442
        rel_y = this->tc_cursor.y - this->tc_top + 1;
×
2443
    } else {
2444
        std::reverse(possibilities.begin(), possibilities.end());
×
2445
        new_sel = vis_line_t(possibilities.size() - 1);
×
2446
    }
2447

2448
    this->tc_complete_range
2449
        = selected_range::from_key(this->tc_cursor.copy_with_x(crange.lr_start),
×
2450
                                   this->tc_cursor.copy_with_x(crange.lr_end));
×
2451
    this->tc_popup_source.replace_with(possibilities);
×
2452
    this->tc_popup.set_window(this->tc_window);
×
2453
    this->tc_popup.set_x(this->vc_x + rel_x);
×
2454
    this->tc_popup.set_y(this->vc_y + rel_y);
×
2455
    this->tc_popup.set_width(full_width);
×
2456
    this->tc_popup.set_height(popup_height);
×
2457
    this->tc_popup.set_visible(true);
×
2458
    this->tc_popup.set_top(0_vl);
×
2459
    this->tc_popup.set_selection(new_sel);
×
2460
    this->set_needs_update();
×
2461
}
2462

2463
void
2464
textinput_curses::open_popup_for_history(std::vector<attr_line_t> possibilities)
×
2465
{
2466
    if (possibilities.empty()) {
×
2467
        this->tc_popup_type = popup_type_t::none;
×
2468
        return;
×
2469
    }
2470

2471
    this->tc_popup_type = popup_type_t::history;
×
2472
    auto new_sel = 0_vl;
×
2473
    auto popup_height = vis_line_t(
2474
        std::min(this->tc_max_popup_height, possibilities.size() + 1));
×
2475
    auto rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2476
    if (this->vc_y + rel_y < 0) {
×
2477
        rel_y = this->tc_cursor.y - this->tc_top - popup_height;
×
2478
    } else {
2479
        std::reverse(possibilities.begin(), possibilities.end());
×
2480
        new_sel = vis_line_t(possibilities.size() - 1);
×
2481
    }
2482

2483
    this->tc_complete_range = selected_range::from_key(
×
2484
        input_point::home(),
2485
        input_point{
2486
            (int) this->tc_lines.back().column_width(),
×
2487
            (int) this->tc_lines.size() - 1,
×
2488
        });
×
2489
    this->tc_popup_source.replace_with(possibilities);
×
2490
    this->tc_popup.set_window(this->tc_window);
×
2491
    this->tc_popup.set_title("History");
×
2492
    this->tc_popup.set_x(this->vc_x);
×
2493
    this->tc_popup.set_y(this->vc_y + rel_y);
×
2494
    this->tc_popup.set_width(this->vc_width);
×
2495
    this->tc_popup.set_height(popup_height);
×
2496
    this->tc_popup.set_top(0_vl);
×
2497
    this->tc_popup.set_selection(new_sel);
×
2498
    this->tc_popup.set_visible(true);
×
2499
    if (this->tc_on_popup_change) {
×
2500
        this->tc_in_popup_change = true;
×
2501
        this->tc_on_popup_change(*this);
×
2502
        this->tc_in_popup_change = false;
×
2503
    }
2504
    this->set_needs_update();
×
2505
}
2506

2507
void
2508
textinput_curses::tick(ui_clock::time_point now)
×
2509
{
2510
    if (this->tc_last_tick_after_input) {
×
2511
        auto diff = now - this->tc_last_tick_after_input.value();
×
2512

2513
        if (diff >= 750ms && !this->tc_timeout_fired) {
×
2514
            if (this->tc_on_timeout) {
×
2515
                this->tc_on_timeout(*this);
×
2516
            }
2517
            this->tc_timeout_fired = true;
×
2518
        }
2519
    } else {
2520
        this->tc_last_tick_after_input = now;
×
2521
        this->tc_timeout_fired = false;
×
2522
    }
2523
}
2524

2525
int
2526
textinput_curses::get_cursor_offset() const
3✔
2527
{
2528
    if (this->tc_cursor.y < 0
6✔
2529
        || this->tc_cursor.y >= (ssize_t) this->tc_lines.size())
3✔
2530
    {
2531
        // XXX can happen during update_lines() with history/pasted insert
2532
        return 0;
×
2533
    }
2534

2535
    int retval = 0;
3✔
2536
    for (auto row = 0; row < this->tc_cursor.y; row++) {
3✔
2537
        retval += this->tc_lines[row].al_string.size() + 1;
×
2538
    }
2539
    retval += this->tc_cursor.x;
3✔
2540

2541
    return retval;
3✔
2542
}
2543

2544
textinput_curses::input_point
2545
textinput_curses::get_point_for_offset(int offset) const
×
2546
{
2547
    auto retval = input_point::home();
×
2548
    auto row = size_t{0};
×
2549
    for (; row < this->tc_lines.size() && offset > 0; row++) {
×
2550
        if (offset < (ssize_t) this->tc_lines[row].al_string.size() + 1) {
×
2551
            break;
×
2552
        }
2553
        offset -= this->tc_lines[row].al_string.size() + 1;
×
2554
        retval.y += 1;
×
2555
    }
2556
    if (row < this->tc_lines.size()) {
×
2557
        retval.x = this->tc_lines[row].byte_to_column_index(offset);
×
2558
    }
2559

2560
    return retval;
×
2561
}
2562

2563
void
2564
textinput_curses::add_mark(input_point pos,
×
2565
                           const lnav::console::user_message& msg)
2566
{
2567
    if (pos.y < 0 || pos.y >= (ssize_t) this->tc_lines.size()) {
×
2568
        log_error("invalid mark position: %d:%d", pos.x, pos.y);
×
2569
        return;
×
2570
    }
2571

2572
    if (this->tc_marks.count(pos) > 0) {
×
2573
        return;
×
2574
    }
2575

2576
    auto& line = this->tc_lines[pos.y];
×
2577
    auto byte_x = (int) line.column_to_byte_index(pos.x);
×
2578
    auto lr = line_range{byte_x, byte_x + 1};
×
2579
    line.al_attrs.emplace_back(lr, VC_ROLE.value(role_t::VCR_ERROR));
×
2580
    line.al_attrs.emplace_back(lr, VC_STYLE.value(text_attrs::with_reverse()));
×
2581

2582
    this->tc_marks.emplace(pos, msg);
×
2583
}
2584

2585
void
2586
textinput_curses::undo_last_change()
×
2587
{
2588
    if (this->tc_change_log.empty()) {
×
2589
        this->tc_notice = no_changes();
×
2590
        this->set_needs_update();
×
2591
    } else {
2592
        log_debug("undo!");
×
2593
        const auto& ce = this->tc_change_log.back();
×
2594
        auto content_sf = string_fragment::from_str(ce.ce_content);
×
2595
        this->tc_selection = ce.ce_range;
×
2596
        log_debug(" range [%d:%d) - [%d:%d) - %s",
×
2597
                  this->tc_selection->sr_start.x,
2598
                  this->tc_selection->sr_start.y,
2599
                  this->tc_selection->sr_end.x,
2600
                  this->tc_selection->sr_end.y,
2601
                  ce.ce_content.c_str());
2602
        this->replace_selection_no_change(content_sf);
×
2603
        this->tc_change_log.pop_back();
×
2604
    }
2605
}
2606

2607
std::string
2608
textinput_curses::selection_to_string() const
×
2609
{
2610
    std::string retval;
×
2611

2612
    if (!this->tc_selection) {
×
2613
        return retval;
×
2614
    }
2615
    auto range = this->tc_selection;
×
2616
    auto add_nl = false;
×
2617
    for (auto y = range->sr_start.y;
×
2618
         y <= range->sr_end.y && y < this->tc_lines.size();
×
2619
         ++y)
2620
    {
2621
        if (add_nl) {
×
2622
            retval.push_back('\n');
×
2623
        }
2624
        auto sel_range = range->range_for_line(y);
×
2625
        if (!sel_range) {
×
2626
            continue;
×
2627
        }
2628

2629
        const auto& al = this->tc_lines[y];
×
2630
        auto byte_start = al.column_to_byte_index(sel_range->lr_start);
×
2631
        auto byte_end = al.column_to_byte_index(sel_range->lr_end);
×
2632
        auto al_sf = string_fragment::from_str_range(
×
2633
            al.al_string, byte_start, byte_end);
×
2634
        retval += al_sf;
×
2635
        add_nl = true;
×
2636
    }
2637

2638
    return retval;
×
2639
}
×
2640

2641
void
2642
textinput_curses::copy_selection()
×
2643
{
2644
    if (!this->tc_selection) {
×
2645
        return;
×
2646
    }
2647

2648
    auto content = this->selection_to_string();
×
2649
    this->tc_clipboard.clear();
×
2650
    this->tc_cut_location = this->tc_cursor;
×
2651
    this->tc_clipboard.emplace_back(content);
×
2652
    this->sync_to_sysclip();
×
2653
}
2654

2655
void
2656
textinput_curses::cut_selection()
×
2657
{
2658
    if (!this->tc_selection) {
×
2659
        return;
×
2660
    }
2661

2662
    auto range = this->tc_selection;
×
2663
    log_debug("cutting selection [%d:%d) - [%d:%d)",
×
2664
              range->sr_start.x,
2665
              range->sr_start.y,
2666
              range->sr_end.x,
2667
              range->sr_end.y);
2668
    auto new_clip = this->selection_to_string();
×
2669
    this->tc_clipboard.clear();
×
2670
    this->tc_clipboard.emplace_back(new_clip);
×
2671
    this->replace_selection(string_fragment{});
×
2672
    this->sync_to_sysclip();
×
2673
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc