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

tstack / lnav / 19150501229-2652

06 Nov 2025 09:31PM UTC coverage: 69.03% (-0.005%) from 69.035%
19150501229-2652

push

github

tstack
[textview] missed needs update on click

0 of 1 new or added line in 1 file covered. (0.0%)

3 existing lines in 1 file now uncovered.

50697 of 73442 relevant lines covered (69.03%)

436003.38 hits per line

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

59.26
/src/textview_curses.cc
1
/**
2
 * Copyright (c) 2007-2012, 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
#include <string>
32
#include <vector>
33

34
#include "textview_curses.hh"
35

36
#include "base/ansi_scrubber.hh"
37
#include "base/humanize.time.hh"
38
#include "base/injector.hh"
39
#include "base/lnav_log.hh"
40
#include "base/time_util.hh"
41
#include "config.h"
42
#include "data_scanner.hh"
43
#include "fmt/format.h"
44
#include "lnav_config.hh"
45
#include "log_format_fwd.hh"
46
#include "logfile.hh"
47
#include "shlex.hh"
48
#include "view_curses.hh"
49

50
constexpr auto REVERSE_SEARCH_OFFSET = 2000_vl;
51

52
void
53
text_filter::revert_to_last(logfile_filter_state& lfs, size_t rollback_size)
47✔
54
{
55
    require(lfs.tfs_lines_for_message[this->lf_index] == 0);
47✔
56

57
    lfs.tfs_message_matched[this->lf_index]
47✔
58
        = lfs.tfs_last_message_matched[this->lf_index];
47✔
59
    lfs.tfs_lines_for_message[this->lf_index]
47✔
60
        = lfs.tfs_last_lines_for_message[this->lf_index];
47✔
61

62
    for (size_t lpc = 0; lpc < lfs.tfs_lines_for_message[this->lf_index]; lpc++)
194✔
63
    {
64
        if (lfs.tfs_message_matched[this->lf_index]) {
147✔
65
            lfs.tfs_filter_hits[this->lf_index] -= 1;
3✔
66
        }
67
        lfs.tfs_filter_count[this->lf_index] -= 1;
147✔
68
        size_t line_number = lfs.tfs_filter_count[this->lf_index];
147✔
69

70
        lfs.tfs_mask[line_number] &= ~(((uint32_t) 1) << this->lf_index);
147✔
71
    }
72
    if (lfs.tfs_lines_for_message[this->lf_index] > 0) {
47✔
73
        require(lfs.tfs_lines_for_message[this->lf_index] >= rollback_size);
46✔
74

75
        lfs.tfs_lines_for_message[this->lf_index] -= rollback_size;
46✔
76
    }
77
    if (lfs.tfs_lines_for_message[this->lf_index] == 0) {
47✔
78
        lfs.tfs_message_matched[this->lf_index] = false;
47✔
79
    }
80
}
47✔
81

82
bool
83
text_filter::add_line(logfile_filter_state& lfs,
1,350✔
84
                      logfile::const_iterator ll,
85
                      const shared_buffer_ref& line)
86
{
87
    if (ll->is_message()) {
1,350✔
88
        this->end_of_message(lfs);
727✔
89
    }
90
    auto retval = this->matches(line_source{*lfs.tfs_logfile, ll}, line);
1,350✔
91

92
    lfs.tfs_message_matched[this->lf_index]
1,350✔
93
        = lfs.tfs_message_matched[this->lf_index] || retval;
1,350✔
94
    lfs.tfs_lines_for_message[this->lf_index] += 1;
1,350✔
95

96
    return retval;
1,350✔
97
}
98

99
void
100
text_filter::end_of_message(logfile_filter_state& lfs)
880✔
101
{
102
    uint32_t mask = 0;
880✔
103

104
    mask = ((uint32_t) 1U << this->lf_index);
880✔
105

106
    for (size_t lpc = 0; lpc < lfs.tfs_lines_for_message[this->lf_index]; lpc++)
2,230✔
107
    {
108
        require(lfs.tfs_filter_count[this->lf_index]
1,350✔
109
                <= lfs.tfs_logfile->size());
110

111
        size_t line_number = lfs.tfs_filter_count[this->lf_index];
1,350✔
112

113
        if (lfs.tfs_message_matched[this->lf_index]) {
1,350✔
114
            lfs.tfs_mask[line_number] |= mask;
133✔
115
        } else {
116
            lfs.tfs_mask[line_number] &= ~mask;
1,217✔
117
        }
118
        lfs.tfs_filter_count[this->lf_index] += 1;
1,350✔
119
        if (lfs.tfs_message_matched[this->lf_index]) {
1,350✔
120
            lfs.tfs_filter_hits[this->lf_index] += 1;
133✔
121
        }
122
    }
123
    lfs.tfs_last_message_matched[this->lf_index]
880✔
124
        = lfs.tfs_message_matched[this->lf_index];
880✔
125
    lfs.tfs_last_lines_for_message[this->lf_index]
880✔
126
        = lfs.tfs_lines_for_message[this->lf_index];
880✔
127
    lfs.tfs_message_matched[this->lf_index] = false;
880✔
128
    lfs.tfs_lines_for_message[this->lf_index] = 0;
880✔
129
}
880✔
130

131
void
132
text_time_translator::add_time_commands_for_session(
282✔
133
    const std::function<void(const std::string&)>& receiver)
134
{
135
    auto min_time_opt = this->get_min_row_time();
282✔
136
    if (min_time_opt) {
282✔
137
        auto min_time_str = lnav::to_rfc3339_string(min_time_opt.value(), 'T');
3✔
138
        receiver(fmt::format(FMT_STRING("hide-lines-before {}"), min_time_str));
12✔
139
    }
3✔
140
    auto max_time_opt = this->get_max_row_time();
282✔
141
    if (max_time_opt) {
282✔
142
        auto max_time_str = lnav::to_rfc3339_string(max_time_opt.value(), 'T');
×
143
        receiver(fmt::format(FMT_STRING("hide-lines-after {}"), max_time_str));
×
144
    }
145
}
282✔
146

147
log_accel::direction_t
148
text_accel_source::get_line_accel_direction(vis_line_t vl)
211✔
149
{
150
    log_accel la;
211✔
151

152
    while (vl >= 0) {
1,864✔
153
        const auto* curr_line = this->text_accel_get_line(vl);
1,855✔
154

155
        if (!curr_line->is_message()) {
1,855✔
156
            --vl;
×
157
            continue;
×
158
        }
159

160
        if (!la.add_point(
1,855✔
161
                curr_line->get_time<std::chrono::milliseconds>().count()))
3,710✔
162
        {
163
            break;
202✔
164
        }
165

166
        --vl;
1,653✔
167
    }
168

169
    return la.get_direction();
422✔
170
}
171

172
std::string
173
text_accel_source::get_time_offset_for_line(textview_curses& tc, vis_line_t vl)
211✔
174
{
175
    auto ll = this->text_accel_get_line(vl);
211✔
176
    auto curr_tv = ll->get_timeval();
211✔
177
    timeval diff_tv;
178

179
    auto prev_umark = tc.get_bookmarks()[&textview_curses::BM_USER].prev(vl);
211✔
180
    auto next_umark = tc.get_bookmarks()[&textview_curses::BM_USER].next(vl);
211✔
181
    auto prev_emark
182
        = tc.get_bookmarks()[&textview_curses::BM_USER_EXPR].prev(vl);
211✔
183
    auto next_emark
184
        = tc.get_bookmarks()[&textview_curses::BM_USER_EXPR].next(vl);
211✔
185
    if (!prev_umark && !prev_emark && (next_umark || next_emark)) {
211✔
186
        auto next_line = this->text_accel_get_line(
×
187
            std::max(next_umark.value_or(0_vl), next_emark.value_or(0_vl)));
×
188

189
        diff_tv = curr_tv - next_line->get_timeval();
×
190
    } else {
191
        auto prev_row
192
            = std::max(prev_umark.value_or(0_vl), prev_emark.value_or(0_vl));
211✔
193
        auto* first_line = this->text_accel_get_line(prev_row);
211✔
194
        auto start_tv = first_line->get_timeval();
211✔
195
        diff_tv = curr_tv - start_tv;
211✔
196
    }
197

198
    return humanize::time::duration::from_tv(diff_tv).to_string();
211✔
199
}
200

201
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_ERRORS("error");
202
const DIST_SLICE(bm_types)
203
    bookmark_type_t textview_curses::BM_WARNINGS("warning");
204
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_USER("user");
205
const DIST_SLICE(bm_types)
206
    bookmark_type_t textview_curses::BM_USER_EXPR("user-expr");
207
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_SEARCH("search");
208
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_META("meta");
209
const DIST_SLICE(bm_types)
210
    bookmark_type_t textview_curses::BM_PARTITION("partition");
211

212
textview_curses::textview_curses()
23,288✔
213
    : lnav_config_listener(__FILE__), tc_search_action(noop_func{})
23,288✔
214
{
215
    this->set_data_source(this);
23,288✔
216
}
23,288✔
217

218
textview_curses::~textview_curses()
23,288✔
219
{
220
    this->tc_search_action = noop_func{};
23,288✔
221
}
23,288✔
222

223
void
224
textview_curses::reload_config(error_reporter& reporter)
13,164✔
225
{
226
    const static auto DEFAULT_THEME_NAME = std::string("default");
14,430✔
227
    const auto& vc = view_colors::singleton();
13,164✔
228

229
    for (auto iter = this->tc_highlights.begin();
13,164✔
230
         iter != this->tc_highlights.end();)
16,141✔
231
    {
232
        if (iter->first.first != highlight_source_t::THEME) {
2,977✔
233
            ++iter;
1,081✔
234
            continue;
1,081✔
235
        }
236

237
        iter = this->tc_highlights.erase(iter);
1,896✔
238
    }
239

240
    for (const auto& theme_name : {DEFAULT_THEME_NAME, lnav_config.lc_ui_theme})
65,820✔
241
    {
242
        auto theme_iter = lnav_config.lc_ui_theme_defs.find(theme_name);
26,328✔
243

244
        if (theme_iter == lnav_config.lc_ui_theme_defs.end()) {
26,328✔
245
            continue;
42✔
246
        }
247

248
        auto vars = &theme_iter->second.lt_vars;
26,286✔
249
        for (const auto& hl_pair : theme_iter->second.lt_highlights) {
131,182✔
250
            if (hl_pair.second.hc_regex.pp_value == nullptr) {
104,896✔
251
                continue;
×
252
            }
253

254
            const auto& sc = hl_pair.second.hc_style;
104,896✔
255
            std::string fg_color, bg_color, errmsg;
104,896✔
256
            bool invalid = false;
104,896✔
257
            text_attrs attrs;
104,896✔
258

259
            auto fg1 = sc.sc_color;
104,896✔
260
            auto bg1 = sc.sc_background_color;
104,896✔
261
            shlex(fg1).eval(fg_color, scoped_resolver{vars});
104,896✔
262
            shlex(bg1).eval(bg_color, scoped_resolver{vars});
104,896✔
263

264
            attrs.ta_fg_color = vc.match_color(
104,896✔
265
                styling::color_unit::from_str(fg_color).unwrapOrElse(
209,792✔
266
                    [&](const auto& msg) {
×
267
                        reporter(&sc.sc_color,
×
268
                                 lnav::console::user_message::error(
269
                                     attr_line_t("invalid color -- ")
×
270
                                         .append_quoted(sc.sc_color))
×
271
                                     .with_reason(msg));
×
272
                        invalid = true;
×
273
                        return styling::color_unit::EMPTY;
×
274
                    }));
275
            attrs.ta_bg_color = vc.match_color(
104,896✔
276
                styling::color_unit::from_str(bg_color).unwrapOrElse(
209,792✔
277
                    [&](const auto& msg) {
×
278
                        reporter(&sc.sc_background_color,
×
279
                                 lnav::console::user_message::error(
280
                                     attr_line_t("invalid background color -- ")
×
281
                                         .append_quoted(sc.sc_background_color))
×
282
                                     .with_reason(msg));
×
283
                        invalid = true;
×
284
                        return styling::color_unit::EMPTY;
×
285
                    }));
286
            if (invalid) {
104,896✔
287
                continue;
×
288
            }
289

290
            if (sc.sc_bold) {
104,896✔
291
                attrs |= text_attrs::style::bold;
×
292
            }
293
            if (sc.sc_underline) {
104,896✔
294
                attrs |= text_attrs::style::underline;
×
295
            }
296
            this->tc_highlights[{highlight_source_t::THEME, hl_pair.first}]
209,792✔
297
                = highlighter(hl_pair.second.hc_regex.pp_value)
209,792✔
298
                      .with_attrs(attrs)
104,896✔
299
                      .with_nestable(hl_pair.second.hc_nestable);
209,792✔
300
        }
104,896✔
301
    }
39,492✔
302

303
    if (this->tc_reload_config_delegate) {
13,164✔
304
        this->tc_reload_config_delegate(*this);
138✔
305
    }
306
}
13,164✔
307

308
void
309
textview_curses::invoke_scroll()
41,997✔
310
{
311
    this->tc_selected_text = std::nullopt;
41,997✔
312
    if (this->tc_sub_source != nullptr) {
41,997✔
313
        this->tc_sub_source->scroll_invoked(this);
18,709✔
314
    }
315

316
    listview_curses::invoke_scroll();
41,997✔
317
}
41,997✔
318

319
void
320
textview_curses::reload_data()
52,248✔
321
{
322
    this->tc_selected_text = std::nullopt;
52,248✔
323
    if (this->tc_sub_source != nullptr) {
52,248✔
324
        this->tc_sub_source->text_update_marks(this->tc_bookmarks);
18,500✔
325

326
        auto* ttt = dynamic_cast<text_time_translator*>(this->tc_sub_source);
18,500✔
327

328
        if (ttt != nullptr) {
18,500✔
329
            ttt->data_reloaded(this);
11,730✔
330
        }
331
        listview_curses::reload_data();
18,500✔
332
    }
333
}
52,248✔
334

335
void
336
textview_curses::grep_begin(grep_proc<vis_line_t>& gp,
46✔
337
                            vis_line_t start,
338
                            vis_line_t stop)
339
{
340
    require(this->tc_searching >= 0);
46✔
341

342
    auto op_guard = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
46✔
343

344
    this->tc_searching += 1;
46✔
345
    this->tc_search_action(this);
46✔
346

347
    if (start != -1_vl) {
46✔
348
        auto& search_bv = this->tc_bookmarks[&BM_SEARCH];
34✔
349
        auto pair = search_bv.equal_range(start, stop);
34✔
350

351
        if (pair.first != pair.second) {
34✔
352
            this->set_needs_update();
1✔
353
        }
354
        for (auto mark_iter = pair.first; mark_iter != pair.second; ++mark_iter)
35✔
355
        {
356
            if (this->tc_sub_source) {
1✔
357
                this->tc_sub_source->text_mark(&BM_SEARCH, *mark_iter, false);
1✔
358
            }
359
        }
360
        if (pair.first != pair.second) {
34✔
361
            auto to_del = std::vector<vis_line_t>{};
1✔
362
            for (auto file_iter = pair.first; file_iter != pair.second;
2✔
363
                 ++file_iter)
1✔
364
            {
365
                to_del.emplace_back(*file_iter);
1✔
366
            }
367

368
            for (auto cl : to_del) {
2✔
369
                search_bv.bv_tree.erase(cl);
1✔
370
            }
371
        }
1✔
372
    }
373

374
    listview_curses::reload_data();
46✔
375
}
46✔
376

377
void
378
textview_curses::grep_end_batch(grep_proc<vis_line_t>& gp)
60✔
379
{
380
    auto op_guard = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
60✔
381

382
    if (this->tc_follow_deadline.tv_sec
120✔
383
        && this->tc_follow_selection == this->get_selection())
60✔
384
    {
385
        timeval now;
386

387
        gettimeofday(&now, nullptr);
×
388
        if (this->tc_follow_deadline < now) {
×
389
        } else {
390
            if (this->tc_follow_func) {
×
391
                if (this->tc_follow_func()) {
×
392
                    this->tc_follow_deadline = {0, 0};
×
393
                }
394
            } else {
395
                this->tc_follow_deadline = {0, 0};
×
396
            }
397
        }
398
    }
399
    this->tc_search_action(this);
60✔
400
}
60✔
401

402
void
403
textview_curses::grep_end(grep_proc<vis_line_t>& gp)
46✔
404
{
405
    auto op_guard = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
46✔
406

407
    this->tc_searching -= 1;
46✔
408
    this->grep_end_batch(gp);
46✔
409
    if (this->tc_searching == 0 && this->tc_search_start_time) {
46✔
410
        const auto now = std::chrono::steady_clock::now();
9✔
411
        this->tc_search_duration
412
            = std::chrono::duration_cast<std::chrono::milliseconds>(
9✔
413
                now - this->tc_search_start_time.value());
18✔
414
        this->tc_search_start_time = std::nullopt;
9✔
415
        if (this->tc_state_event_handler) {
9✔
416
            this->tc_state_event_handler(*this);
×
417
        }
418
    }
419

420
    ensure(this->tc_searching >= 0);
46✔
421
}
46✔
422

423
void
424
textview_curses::grep_match(grep_proc<vis_line_t>& gp, vis_line_t line)
16✔
425
{
426
    this->tc_bookmarks[&BM_SEARCH].insert_once(vis_line_t(line));
16✔
427
    if (this->tc_sub_source != nullptr) {
16✔
428
        this->tc_sub_source->text_mark(&BM_SEARCH, line, true);
16✔
429
    }
430

431
    if (this->get_top() <= line && line <= this->get_bottom()) {
16✔
432
        listview_curses::reload_data();
16✔
433
    }
434
}
16✔
435

436
void
437
textview_curses::listview_value_for_rows(const listview_curses& lv,
14,272✔
438
                                         vis_line_t row,
439
                                         std::vector<attr_line_t>& rows_out)
440
{
441
    for (auto& al : rows_out) {
28,646✔
442
        this->textview_value_for_row(row, al);
14,374✔
443

444
        auto& sa = al.al_attrs;
14,374✔
445
        if (this->is_selectable() && this->tc_cursor_role
14,829✔
446
            && this->tc_disabled_cursor_role)
14,829✔
447
        {
448
            auto sel_end = this->get_selection().value_or(0_vl);
117✔
449
            auto sel_start = sel_end;
117✔
450
            if (this->tc_selection_start) {
117✔
451
                if (this->tc_selection_start.value() < sel_end) {
×
452
                    sel_start = this->tc_selection_start.value();
×
453
                } else {
454
                    sel_end = this->tc_selection_start.value();
×
455
                }
456
            }
457

458
            if (sel_start <= row && row <= sel_end) {
117✔
459
                auto role = (this->get_overlay_selection() || !this->vc_enabled)
24✔
460
                    ? this->tc_disabled_cursor_role.value()
18✔
461
                    : this->tc_cursor_role.value();
6✔
462

463
                sa.emplace_back(line_range{0, -1}, VC_ROLE.value(role));
12✔
464
            }
465
        }
466

467
        if (this->tc_selected_text) {
14,374✔
468
            const auto& sti = this->tc_selected_text.value();
×
469
            if (sti.sti_line == row) {
×
470
                sa.emplace_back(sti.sti_range,
×
471
                                VC_ROLE.value(role_t::VCR_SELECTED_TEXT));
×
472
            }
473
        }
474

475
        ++row;
14,374✔
476
    }
477
}
14,272✔
478

479
bool
480
textview_curses::handle_mouse(mouse_event& me)
×
481
{
482
    if (!this->vc_visible || this->lv_height == 0) {
×
483
        return false;
×
484
    }
485

486
    if (!this->tc_selection_start && listview_curses::handle_mouse(me)) {
×
487
        return true;
×
488
    }
489

490
    auto mouse_line = (me.me_y < 0 || me.me_y >= this->lv_display_lines.size())
×
491
        ? empty_space{}
×
492
        : this->lv_display_lines[me.me_y];
×
493
    auto [height, width] = this->get_dimensions();
×
494

495
    if (!mouse_line.is<overlay_menu>()
×
496
        && (me.me_button != mouse_button_t::BUTTON_LEFT
×
497
            || me.me_state != mouse_button_state_t::BUTTON_STATE_RELEASED))
×
498
    {
499
        this->tc_selected_text = std::nullopt;
×
500
        this->set_needs_update();
×
501
    }
502

503
    std::optional<int> overlay_content_min_y;
×
504
    std::optional<int> overlay_content_max_y;
×
505
    if (this->tc_press_line.is<overlay_content>()) {
×
506
        auto main_line
507
            = this->tc_press_line.get<overlay_content>().oc_main_line;
×
508
        for (size_t lpc = 0; lpc < this->lv_display_lines.size(); lpc++) {
×
509
            if (overlay_content_min_y
×
510
                && !this->lv_display_lines[lpc].is<static_overlay_content>()
×
511
                && !this->lv_display_lines[lpc].is<overlay_content>())
×
512
            {
513
                overlay_content_max_y = lpc;
×
514
                break;
×
515
            }
516
            if (this->lv_display_lines[lpc].is<main_content>()) {
×
517
                auto& mc = this->lv_display_lines[lpc].get<main_content>();
×
518
                if (mc.mc_line == main_line) {
×
519
                    overlay_content_min_y = lpc;
×
520
                }
521
            }
522
        }
523
        if (overlay_content_min_y && !overlay_content_max_y) {
×
524
            overlay_content_max_y = this->lv_display_lines.size();
×
525
        }
526
    }
527

528
    auto* sub_delegate = dynamic_cast<text_delegate*>(this->tc_sub_source);
×
529

530
    switch (me.me_state) {
×
531
        case mouse_button_state_t::BUTTON_STATE_PRESSED: {
×
532
            this->tc_selection_at_press = this->get_selection();
×
533
            this->tc_press_line = mouse_line;
×
534
            this->tc_press_left = this->lv_left + me.me_press_x;
×
535
            if (!this->lv_selectable) {
×
536
                this->set_selectable(true);
×
537
            }
538
            mouse_line.match(
×
539
                [this, &me, sub_delegate, &mouse_line](const main_content& mc) {
×
540
                    this->tc_text_selection_active = true;
×
541
                    this->tc_press_left = this->lv_left
×
542
                        + mc.mc_line_range.lr_start + me.me_press_x;
×
543
                    if (this->vc_enabled) {
×
544
                        if (this->tc_supports_marks
×
545
                            && me.me_button == mouse_button_t::BUTTON_LEFT
×
546
                            && (me.is_modifier_pressed(
×
547
                                    mouse_event::modifier_t::shift)
548
                                || me.is_modifier_pressed(
×
549
                                    mouse_event::modifier_t::ctrl)))
550
                        {
551
                            this->tc_selection_start = mc.mc_line;
×
552
                        }
553
                        this->set_selection_without_context(mc.mc_line);
×
554
                        this->tc_press_event = me;
×
555
                    }
556
                    if (this->tc_delegate != nullptr) {
×
557
                        this->tc_delegate->text_handle_mouse(
×
558
                            *this, mouse_line, me);
559
                    }
560
                    if (sub_delegate != nullptr) {
×
561
                        sub_delegate->text_handle_mouse(*this, mouse_line, me);
×
562
                    }
563
                },
×
564
                [](const overlay_menu& om) {},
×
565
                [](const static_overlay_content& soc) {},
×
566
                [this](const overlay_content& oc) {
×
567
                    this->set_selection(oc.oc_main_line);
×
568
                    this->set_overlay_selection(oc.oc_line);
×
569
                },
×
570
                [](const empty_space& es) {});
×
571
            break;
×
572
        }
573
        case mouse_button_state_t::BUTTON_STATE_DOUBLE_CLICK: {
×
574
            if (!this->lv_selectable) {
×
575
                this->set_selectable(true);
×
576
            }
577
            this->tc_text_selection_active = false;
×
578
            mouse_line.match(
×
579
                [this, &me, &mouse_line, sub_delegate](const main_content& mc) {
×
580
                    if (this->vc_enabled) {
×
581
                        if (this->tc_supports_marks
×
582
                            && me.me_button == mouse_button_t::BUTTON_LEFT)
×
583
                        {
584
                            attr_line_t al;
×
585

586
                            this->textview_value_for_row(mc.mc_line, al);
×
587
                            auto line_sf
588
                                = string_fragment::from_str(al.get_string());
×
589
                            auto cursor_sf = line_sf.sub_cell_range(
×
590
                                this->lv_left + mc.mc_line_range.lr_start
×
591
                                    + me.me_x,
×
592
                                this->lv_left + mc.mc_line_range.lr_start
×
593
                                    + me.me_x);
×
594
                            auto ds = data_scanner(line_sf);
×
595
                            auto tf = this->tc_sub_source->get_text_format();
×
596
                            while (true) {
597
                                auto tok_res = ds.tokenize2(tf);
×
598
                                if (!tok_res) {
×
599
                                    break;
×
600
                                }
601

602
                                auto tok = tok_res.value();
×
603
                                auto tok_sf
604
                                    = (tok.tr_token
×
605
                                           == data_token_t::DT_QUOTED_STRING
606
                                       && (cursor_sf.sf_begin
×
607
                                               == tok.to_string_fragment()
×
608
                                                      .sf_begin
×
609
                                           || cursor_sf.sf_begin
×
610
                                               == tok.to_string_fragment()
×
611
                                                       .sf_end
×
612
                                                   - 1))
×
613
                                    ? tok.to_string_fragment()
×
614
                                    : tok.inner_string_fragment();
×
615
                                if (tok_sf.contains(cursor_sf)
×
616
                                    && tok.tr_token != data_token_t::DT_WHITE)
×
617
                                {
618
                                    auto group_tok
619
                                        = ds.find_matching_bracket(tf, tok);
×
620
                                    if (group_tok) {
×
621
                                        tok_sf = group_tok.value()
×
622
                                                     .to_string_fragment();
×
623
                                    }
624
                                    this->tc_selected_text = selected_text_info{
×
625
                                        me.me_x,
×
626
                                        mc.mc_line,
×
627
                                        line_range{
628
                                            tok_sf.sf_begin,
629
                                            tok_sf.sf_end,
630
                                        },
631
                                        al.al_attrs,
632
                                        tok_sf.to_string(),
633
                                    };
634
                                    this->set_needs_update();
×
635
                                    break;
×
636
                                }
637
                            }
638
                        }
639
                        this->set_selection_without_context(mc.mc_line);
×
640
                    }
641
                    if (this->tc_delegate != nullptr) {
×
642
                        this->tc_delegate->text_handle_mouse(
×
643
                            *this, mouse_line, me);
644
                    }
645
                    if (sub_delegate != nullptr) {
×
646
                        sub_delegate->text_handle_mouse(*this, mouse_line, me);
×
647
                    }
648
                },
×
649
                [](const static_overlay_content& soc) {},
×
650
                [](const overlay_menu& om) {},
×
651
                [](const overlay_content& oc) {},
×
652
                [](const empty_space& es) {});
×
653
            break;
×
654
        }
655
        case mouse_button_state_t::BUTTON_STATE_DRAGGED: {
×
656
            this->tc_text_selection_active = true;
×
657
            if (!this->vc_enabled) {
×
658
            } else if (me.me_y == me.me_press_y) {
×
659
                if (mouse_line.is<main_content>()) {
×
660
                    auto& mc = mouse_line.get<main_content>();
×
661
                    attr_line_t al;
×
662
                    auto low_x
663
                        = std::min(this->tc_press_left,
×
664
                                   (int) this->lv_left
×
665
                                       + mc.mc_line_range.lr_start + me.me_x);
×
666
                    auto high_x
667
                        = std::max(this->tc_press_left,
×
668
                                   (int) this->lv_left
×
669
                                       + mc.mc_line_range.lr_start + me.me_x);
×
670

671
                    this->set_selection_without_context(mc.mc_line);
×
672
                    if (this->tc_supports_marks
×
673
                        && me.me_button == mouse_button_t::BUTTON_LEFT)
×
674
                    {
675
                        this->textview_value_for_row(mc.mc_line, al);
×
676
                        auto line_sf
677
                            = string_fragment::from_str(al.get_string());
×
678
                        auto cursor_sf = line_sf.sub_cell_range(low_x, high_x);
×
679
                        if (me.me_x <= 1) {
×
680
                            this->set_left(this->lv_left - 1);
×
681
                        } else if (me.me_x >= width - 1) {
×
682
                            this->set_left(this->lv_left + 1);
×
683
                        }
684
                        if (!cursor_sf.empty()) {
×
685
                            this->tc_selected_text = {
686
                                me.me_x,
×
687
                                mc.mc_line,
×
688
                                line_range{
689
                                    cursor_sf.sf_begin,
690
                                    cursor_sf.sf_end,
691
                                },
692
                                al.al_attrs,
693
                                cursor_sf.to_string(),
694
                            };
695
                        }
696
                    }
697
                }
698
            } else {
699
                if (this->tc_press_line.is<main_content>()) {
×
700
                    if (me.me_y < 0) {
×
701
                        this->shift_selection(shift_amount_t::up_line);
×
702
                    } else if (me.me_y >= height) {
×
703
                        this->shift_selection(shift_amount_t::down_line);
×
704
                    } else if (mouse_line.is<main_content>()) {
×
705
                        this->set_selection_without_context(
×
706
                            mouse_line.get<main_content>().mc_line);
×
707
                    }
708
                } else if (this->tc_press_line.is<overlay_content>()
×
709
                           && overlay_content_min_y && overlay_content_max_y)
×
710
                {
711
                    if (me.me_y < overlay_content_min_y.value()) {
×
712
                        this->set_overlay_selection(
×
713
                            this->get_overlay_selection().value_or(0_vl)
×
714
                            - 1_vl);
×
715
                    } else if (me.me_y >= overlay_content_max_y.value()) {
×
716
                        this->set_overlay_selection(
×
717
                            this->get_overlay_selection().value_or(0_vl)
×
718
                            + 1_vl);
×
719
                    } else if (mouse_line.is<overlay_content>()) {
×
720
                        this->set_overlay_selection(
×
721
                            mouse_line.get<overlay_content>().oc_line);
×
722
                    }
723
                }
724
            }
725
            break;
×
726
        }
727
        case mouse_button_state_t::BUTTON_STATE_RELEASED: {
×
728
            auto* ov = this->get_overlay_source();
×
729
            if (ov != nullptr && mouse_line.is<overlay_menu>()
×
730
                && this->tc_selected_text)
×
731
            {
732
                auto& om = mouse_line.get<overlay_menu>();
×
733
                auto& sti = this->tc_selected_text.value();
×
734

735
                for (const auto& mi : ov->los_menu_items) {
×
736
                    if (om.om_line == mi.mi_line
×
737
                        && me.is_click_in(mouse_button_t::BUTTON_LEFT,
×
738
                                          mi.mi_range))
739
                    {
740
                        mi.mi_action(sti.sti_value);
×
741
                        break;
×
742
                    }
743
                }
744
            }
745
            this->tc_text_selection_active = false;
×
746
            if (this->tc_press_line.is<main_content>()
×
747
                && mouse_line.is<main_content>()
×
748
                && me.is_click_in(mouse_button_t::BUTTON_RIGHT, 0, INT_MAX))
×
749
            {
750
                auto* lov = this->get_overlay_source();
×
751
                if (lov != nullptr
×
752
                    && (!lov->get_show_details_in_overlay()
×
753
                        || this->tc_selection_at_press
×
754
                            == this->get_selection()))
×
755
                {
756
                    this->set_show_details_in_overlay(
×
757
                        !lov->get_show_details_in_overlay());
×
758
                }
759
            }
760
            if (this->vc_enabled) {
×
761
                if (this->tc_selection_start) {
×
762
                    this->toggle_user_mark(&BM_USER,
×
763
                                           this->tc_selection_start.value(),
×
764
                                           this->get_selection().value());
×
765
                    this->reload_data();
×
766
                }
767
                this->tc_selection_start = std::nullopt;
×
768
            }
769
            if (mouse_line.is<main_content>()) {
×
770
                const auto mc = mouse_line.get<main_content>();
×
771
                attr_line_t al;
×
772

773
                this->textview_value_for_row(mc.mc_line, al);
×
774
                auto line_sf = string_fragment::from_str(al.get_string());
×
775
                auto cursor_sf = line_sf.sub_cell_range(
×
776
                    this->lv_left + me.me_x, this->lv_left + me.me_x);
×
777
                auto link_iter = find_string_attr_containing(
×
778
                    al.get_attrs(), &VC_HYPERLINK, cursor_sf.sf_begin);
×
779
                if (link_iter != al.get_attrs().end()) {
×
780
                    auto href = link_iter->sa_value.get<std::string>();
×
781
                    auto* ta = dynamic_cast<text_anchors*>(this->tc_sub_source);
×
782

783
                    if (me.me_button == mouse_button_t::BUTTON_LEFT
×
784
                        && ta != nullptr && startswith(href, "#")
×
785
                        && !startswith(href, "#/frontmatter"))
×
786
                    {
787
                        auto row_opt = ta->row_for_anchor(href);
×
788

789
                        if (row_opt.has_value()) {
×
790
                            this->set_selection(row_opt.value());
×
791
                        }
792
                    } else {
793
                        this->tc_selected_text = selected_text_info{
×
794
                            me.me_x,
×
795
                            mc.mc_line,
×
796
                            link_iter->sa_range,
×
797
                            al.get_attrs(),
×
798
                            al.to_string_fragment(link_iter).to_string(),
×
799
                            href,
800
                        };
NEW
801
                        this->set_needs_update();
×
802
                    }
803
                }
804
                if (this->tc_on_click) {
×
805
                    this->tc_on_click(*this, al, cursor_sf.sf_begin);
×
806
                }
807
            }
808
            if (mouse_line.is<overlay_content>()) {
×
809
                const auto& oc = mouse_line.get<overlay_content>();
×
810
                std::vector<attr_line_t> ov_lines;
×
811

812
                this->lv_overlay_source->list_value_for_overlay(
×
813
                    *this, oc.oc_main_line, ov_lines);
814
                const auto& al = ov_lines[oc.oc_line];
×
815
                auto line_sf = string_fragment::from_str(al.get_string());
×
816
                auto cursor_sf = line_sf.sub_cell_range(
×
817
                    this->lv_left + me.me_x, this->lv_left + me.me_x);
×
818
                auto link_iter = find_string_attr_containing(
×
819
                    al.get_attrs(), &VC_HYPERLINK, cursor_sf.sf_begin);
820
                if (link_iter != al.get_attrs().end()) {
×
821
                    auto href = link_iter->sa_value.get<std::string>();
×
822
                    auto* ta = dynamic_cast<text_anchors*>(this->tc_sub_source);
×
823

824
                    if (me.me_button == mouse_button_t::BUTTON_LEFT
×
825
                        && ta != nullptr && startswith(href, "#")
×
826
                        && !startswith(href, "#/frontmatter"))
×
827
                    {
828
                        auto row_opt = ta->row_for_anchor(href);
×
829

830
                        if (row_opt.has_value()) {
×
831
                            this->tc_sub_source->get_location_history() |
×
832
                                [&oc](auto lh) {
×
833
                                    lh->loc_history_append(oc.oc_main_line);
×
834
                                };
835
                            this->set_selection(row_opt.value());
×
836
                        }
837
                    }
838
                }
839
                if (this->tc_on_click) {
×
840
                    this->tc_on_click(*this, al, cursor_sf.sf_begin);
×
841
                }
842
            }
843
            if (this->tc_delegate != nullptr) {
×
844
                this->tc_delegate->text_handle_mouse(*this, mouse_line, me);
×
845
            }
846
            if (sub_delegate != nullptr) {
×
847
                sub_delegate->text_handle_mouse(*this, mouse_line, me);
×
848
            }
849
            if (mouse_line.is<overlay_menu>()) {
×
850
                this->tc_selected_text = std::nullopt;
×
851
                this->set_needs_update();
×
852
            }
853
            break;
×
854
        }
855
    }
856

857
    return true;
×
858
}
859

860
void
861
textview_curses::apply_highlights(attr_line_t& al,
13,537✔
862
                                  const line_range& body,
863
                                  const line_range& orig_line)
864
{
865
    intern_string_t format_name;
13,537✔
866

867
    auto format_attr_opt = get_string_attr(al.al_attrs, SA_FORMAT);
13,537✔
868
    if (format_attr_opt.has_value()) {
13,537✔
869
        format_name = format_attr_opt.value().get();
3,890✔
870
    }
871

872
    auto source_format = this->tc_sub_source->get_text_format();
13,537✔
873
    if (source_format == text_format_t::TF_BINARY) {
13,537✔
874
        return;
1,430✔
875
    }
876
    for (const auto& tc_highlight : this->tc_highlights) {
136,200✔
877
        bool internal_hl
124,093✔
878
            = tc_highlight.first.first == highlight_source_t::INTERNAL
124,093✔
879
            || tc_highlight.first.first == highlight_source_t::THEME;
124,093✔
880

881
        if (!tc_highlight.second.applies_to_format(source_format)) {
124,093✔
882
            continue;
44,253✔
883
        }
884

885
        if (!tc_highlight.second.h_format_name.empty()
79,864✔
886
            && tc_highlight.second.h_format_name != format_name)
79,864✔
887
        {
888
            continue;
×
889
        }
890

891
        if (this->tc_disabled_highlights.is_set(tc_highlight.first.first)) {
79,864✔
892
            continue;
24✔
893
        }
894

895
        // Internal highlights should only apply to the log message body so
896
        // that we don't start highlighting other fields.  User-provided
897
        // highlights should apply only to the line itself and not any of
898
        // the surrounding decorations that are added (for example, the file
899
        // lines that are inserted at the beginning of the log view).
900
        auto lr = internal_hl ? body : orig_line;
79,840✔
901
        tc_highlight.second.annotate(al, lr);
79,840✔
902
    }
903
}
904

905
void
906
textview_curses::textview_value_for_row(vis_line_t row, attr_line_t& value_out)
14,440✔
907
{
908
    auto& sa = value_out.get_attrs();
14,440✔
909
    auto& str = value_out.get_string();
14,440✔
910

911
    this->tc_sub_source->text_value_for_line(*this, row, str);
14,440✔
912
    this->tc_sub_source->text_attrs_for_line(*this, row, sa);
14,440✔
913

914
    for (const auto& attr : sa) {
112,159✔
915
        require_ge(attr.sa_range.lr_start, 0);
97,719✔
916
    }
917

918
    line_range body, orig_line;
14,440✔
919

920
    body = find_string_attr_range(sa, &SA_BODY);
14,440✔
921
    if (!body.is_valid()) {
14,440✔
922
        body.lr_start = 0;
11,337✔
923
        body.lr_end = str.size();
11,337✔
924
    }
925

926
    orig_line = find_string_attr_range(sa, &SA_ORIGINAL_LINE);
14,440✔
927
    if (!orig_line.is_valid()) {
14,440✔
928
        orig_line.lr_start = 0;
9,856✔
929
        orig_line.lr_end = str.size();
9,856✔
930
    }
931

932
    if (!body.empty() || !orig_line.empty()) {
14,440✔
933
        this->apply_highlights(value_out, body, orig_line);
12,451✔
934
    }
935

936
    if (this->tc_hide_fields) {
14,440✔
937
        value_out.apply_hide();
14,440✔
938
    }
939

940
    const auto& user_marks = this->tc_bookmarks[&BM_USER];
14,440✔
941
    const auto& user_expr_marks = this->tc_bookmarks[&BM_USER_EXPR];
14,440✔
942
    if (this->tc_mark_style
14,440✔
943
        && (user_marks.bv_tree.exists(row)
28,862✔
944
            || user_expr_marks.bv_tree.exists(row)))
28,862✔
945
    {
946
        sa.emplace_back(line_range{orig_line.lr_start, -1},
35✔
947
                        this->tc_mark_style.value());
948
    }
949
}
14,440✔
950

951
void
952
textview_curses::execute_search(const std::string& regex_orig)
26✔
953
{
954
    static auto op = lnav_operation{"grep"};
26✔
955

956
    std::string regex = regex_orig;
26✔
957

958
    if ((this->tc_search_child == nullptr)
26✔
959
        || (regex != this->tc_current_search))
26✔
960
    {
961
        auto op_guard = lnav_opid_guard::async(op);
26✔
962
        std::shared_ptr<lnav::pcre2pp::code> code;
26✔
963
        this->match_reset();
26✔
964

965
        this->tc_search_op_id = std::nullopt;
26✔
966
        this->tc_search_child.reset();
26✔
967
        this->tc_source_search_child.reset();
26✔
968

969
        log_debug("%s: start search for: '%s'",
26✔
970
                  this->vc_title.c_str(),
971
                  regex.c_str());
972

973
        if (regex.empty()) {
26✔
974
        } else {
975
            auto compile_res = lnav::pcre2pp::code::from(regex, PCRE2_CASELESS);
9✔
976

977
            if (compile_res.isErr()) {
9✔
978
                auto ce = compile_res.unwrapErr();
×
979
                regex = lnav::pcre2pp::quote(regex);
×
980

981
                log_info("invalid search regex (%s), using quoted: %s",
×
982
                         ce.get_message().c_str(),
983
                         regex.c_str());
984

985
                auto compile_quote_res
986
                    = lnav::pcre2pp::code::from(regex, PCRE2_CASELESS);
×
987
                if (compile_quote_res.isErr()) {
×
988
                    log_error("Unable to compile quoted regex: %s",
×
989
                              regex.c_str());
990
                } else {
991
                    code = compile_quote_res.unwrap().to_shared();
×
992
                }
993
            } else {
×
994
                code = compile_res.unwrap().to_shared();
9✔
995
            }
996
        }
9✔
997

998
        if (code != nullptr) {
26✔
999
            highlighter hl(code);
9✔
1000

1001
            hl.with_role(role_t::VCR_SEARCH);
9✔
1002

1003
            auto& hm = this->get_highlights();
9✔
1004
            hm[{highlight_source_t::PREVIEW, "search"}] = hl;
9✔
1005

1006
            auto gp = injector::get<std::shared_ptr<grep_proc<vis_line_t>>>(
1007
                code, *this);
9✔
1008

1009
            gp->set_sink(this);
9✔
1010
            auto top = this->get_top();
9✔
1011
            if (top < REVERSE_SEARCH_OFFSET) {
9✔
1012
                top = 0_vl;
9✔
1013
            } else {
1014
                top -= REVERSE_SEARCH_OFFSET;
×
1015
            }
1016
            this->tc_search_op_id = std::move(op_guard).suspend();
9✔
1017
            gp->queue_request(top);
9✔
1018
            if (top > 0) {
9✔
1019
                gp->queue_request(0_vl, top);
×
1020
            }
1021
            this->tc_search_start_time = std::chrono::steady_clock::now();
9✔
1022
            this->tc_search_duration = std::nullopt;
9✔
1023

1024
            auto op_guard
1025
                = lnav_opid_guard::resume(this->tc_search_op_id.value());
9✔
1026
            gp->start();
9✔
1027

1028
            this->tc_search_child = std::make_shared<grep_highlighter>(
9✔
1029
                gp, highlight_source_t::PREVIEW, "search", hm);
9✔
1030

1031
            if (this->tc_sub_source != nullptr) {
9✔
1032
                this->tc_sub_source->get_grepper() | [this, code](auto pair) {
9✔
1033
                    auto sgp
9✔
1034
                        = injector::get<std::shared_ptr<grep_proc<vis_line_t>>>(
1035
                            code, *pair.first);
9✔
1036

1037
                    sgp->set_sink(pair.second);
9✔
1038
                    sgp->queue_request(0_vl);
9✔
1039
                    sgp->start();
9✔
1040

1041
                    this->tc_source_search_child = sgp;
9✔
1042
                };
9✔
1043
            }
1044
        }
9✔
1045
    }
26✔
1046

1047
    this->tc_current_search = regex;
26✔
1048
    if (this->tc_state_event_handler) {
26✔
1049
        this->tc_state_event_handler(*this);
1✔
1050
    }
1051
}
26✔
1052

1053
std::optional<std::pair<int, int>>
1054
textview_curses::horiz_shift(vis_line_t start, vis_line_t end, int off_start)
×
1055
{
1056
    auto hl_iter
1057
        = this->tc_highlights.find({highlight_source_t::PREVIEW, "search"});
×
1058
    if (hl_iter == this->tc_highlights.end()
×
1059
        || hl_iter->second.h_regex == nullptr)
×
1060
    {
1061
        return std::nullopt;
×
1062
    }
1063
    int prev_hit = -1, next_hit = INT_MAX;
×
1064

1065
    for (; start < end; ++start) {
×
1066
        std::vector<attr_line_t> rows(1);
×
1067
        this->listview_value_for_rows(*this, start, rows);
×
1068

1069
        const auto& str = rows[0].get_string();
×
1070
        hl_iter->second.h_regex->capture_from(str).for_each(
×
1071
            [&](lnav::pcre2pp::match_data& md) {
×
1072
                auto cap = md[0].value();
×
1073
                if (cap.sf_begin < off_start) {
×
1074
                    prev_hit = std::max(prev_hit, cap.sf_begin);
×
1075
                } else if (cap.sf_begin > off_start) {
×
1076
                    next_hit = std::min(next_hit, cap.sf_begin);
×
1077
                }
1078
            });
×
1079
    }
1080

1081
    if (prev_hit == -1 && next_hit == INT_MAX) {
×
1082
        return std::nullopt;
×
1083
    }
1084
    return std::make_pair(prev_hit, next_hit);
×
1085
}
1086

1087
void
1088
textview_curses::set_user_mark(const bookmark_type_t* bm,
123✔
1089
                               vis_line_t vl,
1090
                               bool marked)
1091
{
1092
    auto& bv = this->tc_bookmarks[bm];
123✔
1093

1094
    if (marked) {
123✔
1095
        bv.insert_once(vl);
43✔
1096
    } else {
1097
        bv.erase(vl);
80✔
1098
    }
1099
    if (this->tc_sub_source) {
123✔
1100
        this->tc_sub_source->text_mark(bm, vl, marked);
123✔
1101
    }
1102

1103
    if (marked) {
123✔
1104
        this->search_range(vl, vl + 1_vl);
43✔
1105
        this->search_new_data();
43✔
1106
    }
1107
    this->set_needs_update();
123✔
1108
}
123✔
1109

1110
void
1111
textview_curses::toggle_user_mark(const bookmark_type_t* bm,
12✔
1112
                                  vis_line_t start_line,
1113
                                  vis_line_t end_line)
1114
{
1115
    if (end_line == -1) {
12✔
1116
        end_line = start_line;
12✔
1117
    }
1118
    if (start_line > end_line) {
12✔
1119
        std::swap(start_line, end_line);
×
1120
    }
1121

1122
    if (start_line >= this->get_inner_height()) {
12✔
1123
        return;
×
1124
    }
1125
    if (end_line >= this->get_inner_height()) {
12✔
1126
        end_line = vis_line_t(this->get_inner_height() - 1);
×
1127
    }
1128
    for (auto curr_line = start_line; curr_line <= end_line; ++curr_line) {
24✔
1129
        auto& bv = this->tc_bookmarks[bm];
12✔
1130
        auto [insert_iter, added] = bv.insert_once(curr_line);
12✔
1131
        if (!added) {
12✔
1132
            bv.erase(curr_line);
1✔
1133
        }
1134
        if (this->tc_sub_source) {
12✔
1135
            this->tc_sub_source->text_mark(bm, curr_line, added);
12✔
1136
        }
1137
    }
1138
    this->search_range(start_line, end_line + 1_vl);
12✔
1139
    this->search_new_data();
12✔
1140
}
1141

1142
void
1143
textview_curses::redo_search()
1,339✔
1144
{
1145
    if (this->tc_search_child) {
1,339✔
1146
        auto op_guard
1147
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
3✔
1148

1149
        auto* gp = this->tc_search_child->get_grep_proc();
3✔
1150

1151
        gp->invalidate();
3✔
1152
        this->match_reset();
3✔
1153
        gp->queue_request(0_vl).start();
3✔
1154

1155
        if (this->tc_source_search_child) {
3✔
1156
            this->tc_source_search_child->invalidate()
3✔
1157
                .queue_request(0_vl)
6✔
1158
                .start();
3✔
1159
        }
1160
    }
3✔
1161
}
1,339✔
1162

1163
void
1164
textview_curses::search_range(vis_line_t start, vis_line_t stop)
214✔
1165
{
1166
    if (this->tc_search_child) {
214✔
1167
        auto op_guard
1168
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
11✔
1169
        this->tc_search_child->get_grep_proc()->queue_request(start, stop);
11✔
1170
    }
11✔
1171
    if (this->tc_source_search_child) {
214✔
1172
        auto op_guard
1173
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
11✔
1174
        this->tc_source_search_child->queue_request(start, stop);
11✔
1175
    }
11✔
1176
}
214✔
1177

1178
void
1179
textview_curses::search_new_data(vis_line_t start)
158✔
1180
{
1181
    this->search_range(start);
158✔
1182
    if (this->tc_search_child) {
158✔
1183
        auto op_guard
1184
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
6✔
1185

1186
        this->tc_search_child->get_grep_proc()->start();
6✔
1187
    }
6✔
1188
    if (this->tc_source_search_child) {
158✔
1189
        auto op_guard
1190
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
6✔
1191

1192
        this->tc_source_search_child->start();
6✔
1193
    }
6✔
1194
}
158✔
1195

1196
bool
1197
textview_curses::listview_is_row_selectable(const listview_curses& lv,
22✔
1198
                                            vis_line_t row)
1199
{
1200
    if (this->tc_sub_source != nullptr) {
22✔
1201
        return this->tc_sub_source->text_is_row_selectable(*this, row);
22✔
1202
    }
1203

1204
    return true;
×
1205
}
1206

1207
void
1208
textview_curses::listview_selection_changed(const listview_curses& lv)
2,201✔
1209
{
1210
    if (this->tc_sub_source != nullptr) {
2,201✔
1211
        this->tc_sub_source->text_selection_changed(*this);
2,201✔
1212
    }
1213
}
2,201✔
1214

1215
textview_curses&
1216
textview_curses::set_sub_source(text_sub_source* src)
16,387✔
1217
{
1218
    if (this->tc_sub_source != src) {
16,387✔
1219
        this->tc_bookmarks.clear();
13,284✔
1220
        this->tc_sub_source = src;
13,284✔
1221
        if (src) {
13,284✔
1222
            src->register_view(this);
8,871✔
1223
        }
1224
        this->reload_data();
13,284✔
1225
    }
1226
    return *this;
16,387✔
1227
}
1228

1229
std::optional<line_info>
1230
textview_curses::grep_value_for_line(vis_line_t line, std::string& value_out)
3✔
1231
{
1232
    // log_debug("grep line %d", line);
1233
    if (this->tc_sub_source
6✔
1234
        && line < (int) this->tc_sub_source->text_line_count())
3✔
1235
    {
1236
        auto retval = this->tc_sub_source->text_value_for_line(
3✔
1237
            *this, line, value_out, text_sub_source::RF_RAW);
1238
        if (retval.li_utf8_scan_result.is_valid()
3✔
1239
            && retval.li_utf8_scan_result.usr_has_ansi)
3✔
1240
        {
1241
            // log_debug("has ansi %d",
1242
            // retval.li_utf8_scan_result.usr_has_ansi);
1243
            auto new_size = erase_ansi_escapes(value_out);
×
1244
            value_out.resize(new_size);
×
1245
        }
1246
        // log_debug("  line off %lld", retval.li_file_range.fr_offset);
1247
        return retval;
3✔
1248
    }
1249

1250
    return std::nullopt;
×
1251
}
1252

1253
void
1254
textview_curses::update_hash_state(hasher& h) const
10✔
1255
{
1256
    listview_curses::update_hash_state(h);
10✔
1257

1258
    if (this->tc_sub_source != nullptr) {
10✔
1259
        this->tc_sub_source->update_filter_hash_state(h);
10✔
1260
    }
1261
}
10✔
1262

1263
void
1264
textview_curses::clear_preview()
1✔
1265
{
1266
    auto& hl = this->get_highlights();
1✔
1267
    hl.erase({highlight_source_t::PREVIEW, "preview"});
1✔
1268
    hl.erase({highlight_source_t::PREVIEW, "bodypreview"});
1✔
1269
    if (this->tc_sub_source != nullptr) {
1✔
1270
        this->tc_sub_source->clear_preview();
1✔
1271
    }
1272
}
1✔
1273

1274
void
1275
text_sub_source::scroll_invoked(textview_curses* tc)
17,959✔
1276
{
1277
    auto* ttt = dynamic_cast<text_time_translator*>(this);
17,959✔
1278

1279
    if (ttt != nullptr) {
17,959✔
1280
        ttt->ttt_scroll_invoked(tc);
11,180✔
1281
    }
1282
}
17,959✔
1283

1284
void
1285
text_sub_source::clear_preview()
1✔
1286
{
1287
    auto* ttt = dynamic_cast<text_time_translator*>(this);
1✔
1288

1289
    if (ttt != nullptr) {
1✔
1290
        ttt->clear_preview_times();
1✔
1291
    }
1292
}
1✔
1293

1294
void
1295
text_sub_source::add_commands_for_session(
329✔
1296
    const std::function<void(const std::string&)>& receiver)
1297
{
1298
    for (const auto& filter : this->tss_filters) {
335✔
1299
        receiver(filter->to_command());
6✔
1300

1301
        if (!filter->is_enabled()) {
6✔
1302
            receiver(
4✔
1303
                fmt::format(FMT_STRING("disable-filter {}"), filter->get_id()));
16✔
1304
        }
1305
    }
1306

1307
    auto* ttt = dynamic_cast<text_time_translator*>(this);
329✔
1308
    if (ttt != nullptr) {
329✔
1309
        ttt->add_time_commands_for_session(receiver);
282✔
1310
    }
1311

1312
    const auto& hmap = this->tss_view->get_highlights();
329✔
1313
    for (const auto& hl : hmap) {
4,003✔
1314
        if (hl.first.first != highlight_source_t::INTERACTIVE) {
3,674✔
1315
            continue;
3,674✔
1316
        }
1317
        receiver(fmt::format(FMT_STRING("highlight {}"), hl.first.second));
×
1318
    }
1319
}
329✔
1320

1321
void
1322
text_time_translator::ttt_scroll_invoked(textview_curses* tc)
11,180✔
1323
{
1324
    if (tc->get_inner_height() > 0 && tc->get_selection()) {
11,180✔
1325
        this->time_for_row(tc->get_selection().value()) |
1,809✔
1326
            [this](auto new_top_ri) { this->ttt_top_row_info = new_top_ri; };
1,563✔
1327
    }
1328
}
11,180✔
1329

1330
void
1331
text_time_translator::data_reloaded(textview_curses* tc)
11,730✔
1332
{
1333
    if (tc->get_inner_height() == 0) {
11,730✔
1334
        this->ttt_top_row_info = std::nullopt;
9,981✔
1335
        return;
9,981✔
1336
    }
1337
    if (this->ttt_top_row_info) {
1,749✔
1338
        this->row_for(this->ttt_top_row_info.value()) |
407✔
1339
            [tc](auto new_top) { tc->set_selection(new_top); };
399✔
1340
    }
1341
}
1342

1343
template class bookmark_vector<vis_line_t>;
1344

1345
bool
1346
empty_filter::matches(std::optional<line_source> ls,
×
1347
                      const shared_buffer_ref& line)
1348
{
1349
    return false;
×
1350
}
1351

1352
std::string
1353
empty_filter::to_command() const
×
1354
{
1355
    return "";
×
1356
}
1357

1358
std::optional<size_t>
1359
filter_stack::next_index()
94✔
1360
{
1361
    bool used[32];
1362

1363
    memset(used, 0, sizeof(used));
94✔
1364
    for (auto& iter : *this) {
98✔
1365
        if (iter->lf_deleted) {
4✔
1366
            continue;
×
1367
        }
1368

1369
        size_t index = iter->get_index();
4✔
1370

1371
        require(used[index] == false);
4✔
1372

1373
        used[index] = true;
4✔
1374
    }
1375
    for (size_t lpc = this->fs_reserved;
98✔
1376
         lpc < logfile_filter_state::MAX_FILTERS;
98✔
1377
         lpc++)
1378
    {
1379
        if (!used[lpc]) {
98✔
1380
            return lpc;
94✔
1381
        }
1382
    }
1383
    return std::nullopt;
×
1384
}
1385

1386
std::shared_ptr<text_filter>
1387
filter_stack::get_filter(const std::string& id)
36✔
1388
{
1389
    auto iter = this->fs_filters.begin();
36✔
1390
    std::shared_ptr<text_filter> retval;
36✔
1391

1392
    for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {
38✔
1393
    }
1394
    if (iter != this->fs_filters.end()) {
36✔
1395
        retval = *iter;
4✔
1396
    }
1397

1398
    return retval;
72✔
1399
}
×
1400

1401
bool
1402
filter_stack::delete_filter(const std::string& id)
9✔
1403
{
1404
    auto iter = this->fs_filters.begin();
9✔
1405

1406
    for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {
9✔
1407
    }
1408
    if (iter != this->fs_filters.end()) {
9✔
1409
        this->fs_filters.erase(iter);
9✔
1410
        this->fs_generation += 1;
9✔
1411
        return true;
9✔
1412
    }
1413

1414
    return false;
×
1415
}
1416

1417
void
1418
filter_stack::get_mask(uint32_t& filter_mask)
×
1419
{
1420
    filter_mask = 0;
×
1421
    for (auto& iter : *this) {
×
1422
        std::shared_ptr<text_filter> tf = iter;
×
1423

1424
        if (tf->lf_deleted) {
×
1425
            continue;
×
1426
        }
1427
        if (tf->is_enabled()) {
×
1428
            uint32_t bit = (1UL << tf->get_index());
×
1429

1430
            switch (tf->get_type()) {
×
1431
                case text_filter::EXCLUDE:
×
1432
                case text_filter::INCLUDE:
1433
                    filter_mask |= bit;
×
1434
                    break;
×
1435
                default:
×
1436
                    ensure(0);
×
1437
                    break;
1438
            }
1439
        }
1440
    }
1441
}
1442

1443
void
1444
filter_stack::get_enabled_mask(uint32_t& filter_in_mask,
1,942✔
1445
                               uint32_t& filter_out_mask)
1446
{
1447
    filter_in_mask = filter_out_mask = 0;
1,942✔
1448
    for (auto& iter : *this) {
2,203✔
1449
        std::shared_ptr<text_filter> tf = iter;
261✔
1450

1451
        if (tf->lf_deleted) {
261✔
1452
            continue;
6✔
1453
        }
1454
        if (tf->is_enabled()) {
255✔
1455
            uint32_t bit = (1UL << tf->get_index());
106✔
1456

1457
            switch (tf->get_type()) {
106✔
1458
                case text_filter::EXCLUDE:
57✔
1459
                    filter_out_mask |= bit;
57✔
1460
                    break;
57✔
1461
                case text_filter::INCLUDE:
49✔
1462
                    filter_in_mask |= bit;
49✔
1463
                    break;
49✔
1464
                default:
×
1465
                    ensure(0);
×
1466
                    break;
1467
            }
1468
        }
1469
    }
261✔
1470
}
1,942✔
1471

1472
void
1473
filter_stack::add_filter(const std::shared_ptr<text_filter>& filter)
98✔
1474
{
1475
    this->fs_filters.push_back(filter);
98✔
1476
    this->fs_generation += 1;
98✔
1477
}
98✔
1478

1479
void
1480
vis_location_history::loc_history_append(vis_line_t top)
25✔
1481
{
1482
    auto iter = this->vlh_history.begin();
25✔
1483
    iter += this->vlh_history.size() - this->lh_history_position;
25✔
1484
    this->vlh_history.erase_from(iter);
25✔
1485
    this->lh_history_position = 0;
25✔
1486
    this->vlh_history.push_back(top);
25✔
1487
}
25✔
1488

1489
std::optional<vis_line_t>
1490
vis_location_history::loc_history_back(vis_line_t current_top)
×
1491
{
1492
    if (this->lh_history_position == 0) {
×
1493
        vis_line_t history_top = this->current_position();
×
1494
        if (history_top != current_top) {
×
1495
            return history_top;
×
1496
        }
1497
    }
1498

1499
    if (this->lh_history_position + 1 >= this->vlh_history.size()) {
×
1500
        return std::nullopt;
×
1501
    }
1502

1503
    this->lh_history_position += 1;
×
1504

1505
    return this->current_position();
×
1506
}
1507

1508
std::optional<vis_line_t>
1509
vis_location_history::loc_history_forward(vis_line_t current_top)
×
1510
{
1511
    if (this->lh_history_position == 0) {
×
1512
        return std::nullopt;
×
1513
    }
1514

1515
    this->lh_history_position -= 1;
×
1516

1517
    return this->current_position();
×
1518
}
1519

1520
void
1521
text_sub_source::update_filter_hash_state(hasher& h) const
10✔
1522
{
1523
    h.update(this->tss_filters.fs_generation);
10✔
1524

1525
    const auto* ttt = dynamic_cast<const text_time_translator*>(this);
10✔
1526
    if (ttt != nullptr) {
10✔
1527
        auto min_time = ttt->get_min_row_time();
10✔
1528
        if (min_time) {
10✔
1529
            h.update(min_time->tv_sec);
×
1530
            h.update(min_time->tv_usec);
×
1531
        } else {
1532
            h.update(0);
10✔
1533
            h.update(0);
10✔
1534
        }
1535
        auto max_time = ttt->get_max_row_time();
10✔
1536
        if (max_time) {
10✔
1537
            h.update(max_time->tv_sec);
×
1538
            h.update(max_time->tv_usec);
×
1539
        } else {
1540
            h.update(0);
10✔
1541
            h.update(0);
10✔
1542
        }
1543
    }
1544
}
10✔
1545

1546
void
1547
text_sub_source::toggle_apply_filters()
1✔
1548
{
1549
    this->tss_apply_filters = !this->tss_apply_filters;
1✔
1550
    this->text_filters_changed();
1✔
1551
}
1✔
1552

1553
void
1554
text_sub_source::text_crumbs_for_line(int line,
42✔
1555
                                      std::vector<breadcrumb::crumb>& crumbs)
1556
{
1557
}
42✔
1558

1559
logfile_filter_state::logfile_filter_state(std::shared_ptr<logfile> lf)
1,131✔
1560
    : tfs_logfile(std::move(lf))
1,131✔
1561
{
1562
    memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
1,131✔
1563
    memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
1,131✔
1564
    memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
1,131✔
1565
    memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
1,131✔
1566
    memset(this->tfs_last_message_matched,
1,131✔
1567
           0,
1568
           sizeof(this->tfs_last_message_matched));
1569
    memset(this->tfs_last_lines_for_message,
1,131✔
1570
           0,
1571
           sizeof(this->tfs_last_lines_for_message));
1572
    this->tfs_mask.reserve(64 * 1024);
1,131✔
1573
}
1,131✔
1574

1575
void
1576
logfile_filter_state::clear()
515✔
1577
{
1578
    this->tfs_logfile = nullptr;
515✔
1579
    this->clear_for_rebuild();
515✔
1580
}
515✔
1581

1582
void
1583
logfile_filter_state::clear_for_rebuild()
520✔
1584
{
1585
    memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
520✔
1586
    memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
520✔
1587
    memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
520✔
1588
    memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
520✔
1589
    memset(this->tfs_last_message_matched,
520✔
1590
           0,
1591
           sizeof(this->tfs_last_message_matched));
1592
    memset(this->tfs_last_lines_for_message,
520✔
1593
           0,
1594
           sizeof(this->tfs_last_lines_for_message));
1595
    this->tfs_mask.clear();
520✔
1596
    this->tfs_index.clear();
520✔
1597
}
520✔
1598

1599
void
1600
logfile_filter_state::clear_filter_state(size_t index)
3,851✔
1601
{
1602
    this->tfs_filter_count[index] = 0;
3,851✔
1603
    this->tfs_filter_hits[index] = 0;
3,851✔
1604
    this->tfs_message_matched[index] = false;
3,851✔
1605
    this->tfs_lines_for_message[index] = 0;
3,851✔
1606
    this->tfs_last_message_matched[index] = false;
3,851✔
1607
    this->tfs_last_lines_for_message[index] = 0;
3,851✔
1608
}
3,851✔
1609

1610
void
1611
logfile_filter_state::clear_deleted_filter_state(uint32_t used_mask)
121✔
1612
{
1613
    for (int lpc = 0; lpc < MAX_FILTERS; lpc++) {
3,993✔
1614
        if (!(used_mask & (1L << lpc))) {
3,872✔
1615
            this->clear_filter_state(lpc);
3,810✔
1616
        }
1617
    }
1618
    for (size_t lpc = 0; lpc < this->tfs_mask.size(); lpc++) {
1,568✔
1619
        this->tfs_mask[lpc] &= used_mask;
1,447✔
1620
    }
1621
}
121✔
1622

1623
void
1624
logfile_filter_state::resize(size_t newsize)
18,532✔
1625
{
1626
    size_t old_mask_size = this->tfs_mask.size();
18,532✔
1627

1628
    this->tfs_mask.resize(newsize);
18,532✔
1629
    if (newsize > old_mask_size) {
18,532✔
1630
        memset(&this->tfs_mask[old_mask_size],
17,587✔
1631
               0,
1632
               sizeof(uint32_t) * (newsize - old_mask_size));
17,587✔
1633
    }
1634
}
18,532✔
1635

1636
void
1637
logfile_filter_state::reserve(size_t expected)
2,330✔
1638
{
1639
    this->tfs_mask.reserve(expected);
2,330✔
1640
}
2,330✔
1641

1642
std::optional<size_t>
1643
logfile_filter_state::content_line_to_vis_line(uint32_t line)
×
1644
{
1645
    if (this->tfs_index.empty()) {
×
1646
        return std::nullopt;
×
1647
    }
1648

1649
    auto iter = std::lower_bound(
×
1650
        this->tfs_index.begin(), this->tfs_index.end(), line);
1651

1652
    if (iter == this->tfs_index.end() || *iter != line) {
×
1653
        return std::nullopt;
×
1654
    }
1655

1656
    return std::make_optional(std::distance(this->tfs_index.begin(), iter));
×
1657
}
1658

1659
std::string
1660
text_anchors::to_anchor_string(const std::string& raw)
60✔
1661
{
1662
    static const auto ANCHOR_RE = lnav::pcre2pp::code::from_const(R"([^\w]+)");
60✔
1663

1664
    return fmt::format(FMT_STRING("#{}"), ANCHOR_RE.replace(tolower(raw), "-"));
240✔
1665
}
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