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

tstack / lnav / 23418727412-2872

23 Mar 2026 02:22AM UTC coverage: 69.027% (+0.02%) from 69.01%
23418727412-2872

push

github

tstack
[tests] add expected for filter test

52768 of 76445 relevant lines covered (69.03%)

522597.39 hits per line

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

60.47
/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)
49✔
54
{
55
    require(lfs.tfs_lines_for_message[this->lf_index] == 0);
49✔
56

57
    lfs.tfs_message_matched[this->lf_index]
49✔
58
        = lfs.tfs_last_message_matched[this->lf_index];
49✔
59
    lfs.tfs_lines_for_message[this->lf_index]
49✔
60
        = lfs.tfs_last_lines_for_message[this->lf_index];
49✔
61
    lfs.tfs_hits_for_message[this->lf_index]
49✔
62
        = lfs.tfs_last_hits_for_message[this->lf_index];
49✔
63

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

72
        lfs.tfs_mask[line_number] &= ~(((uint32_t) 1) << this->lf_index);
149✔
73
    }
74
    if (lfs.tfs_lines_for_message[this->lf_index] > 0) {
49✔
75
        require(lfs.tfs_lines_for_message[this->lf_index] >= rollback_size);
48✔
76

77
        lfs.tfs_lines_for_message[this->lf_index] -= rollback_size;
48✔
78
    }
79
    if (lfs.tfs_lines_for_message[this->lf_index] == 0) {
49✔
80
        lfs.tfs_message_matched[this->lf_index] = false;
49✔
81
    }
82
}
49✔
83

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

94
    lfs.tfs_message_matched[this->lf_index]
1,455✔
95
        = lfs.tfs_message_matched[this->lf_index] || retval;
1,455✔
96
    lfs.tfs_lines_for_message[this->lf_index] += 1;
1,455✔
97
    if (retval) {
1,455✔
98
        lfs.tfs_hits_for_message[this->lf_index] += 1;
133✔
99
    }
100

101
    return retval;
1,455✔
102
}
103

104
void
105
text_filter::end_of_message(logfile_filter_state& lfs)
908✔
106
{
107
    uint32_t mask = 0;
908✔
108

109
    mask = ((uint32_t) 1U << this->lf_index);
908✔
110

111
    for (size_t lpc = 0; lpc < lfs.tfs_lines_for_message[this->lf_index]; lpc++)
2,363✔
112
    {
113
        size_t line_number = lfs.tfs_filter_count[this->lf_index];
1,455✔
114
        require(line_number <= lfs.tfs_logfile->size());
1,455✔
115

116
        if (line_number == lfs.tfs_logfile->size()) {
1,455✔
117
            continue;
×
118
        }
119
        if (lfs.tfs_message_matched[this->lf_index]) {
1,455✔
120
            lfs.tfs_mask[line_number] |= mask;
179✔
121
        } else {
122
            lfs.tfs_mask[line_number] &= ~mask;
1,276✔
123
        }
124
        lfs.tfs_filter_count[this->lf_index] += 1;
1,455✔
125
    }
126
    lfs.tfs_filter_hits[this->lf_index]
908✔
127
        += lfs.tfs_hits_for_message[this->lf_index];
908✔
128
    lfs.tfs_last_message_matched[this->lf_index]
908✔
129
        = lfs.tfs_message_matched[this->lf_index];
908✔
130
    lfs.tfs_last_lines_for_message[this->lf_index]
908✔
131
        = lfs.tfs_lines_for_message[this->lf_index];
908✔
132
    lfs.tfs_last_hits_for_message[this->lf_index]
908✔
133
        = lfs.tfs_hits_for_message[this->lf_index];
908✔
134
    lfs.tfs_message_matched[this->lf_index] = false;
908✔
135
    lfs.tfs_lines_for_message[this->lf_index] = 0;
908✔
136
    lfs.tfs_hits_for_message[this->lf_index] = 0;
908✔
137
}
908✔
138

139
void
140
text_time_translator::add_time_commands_for_session(
294✔
141
    const std::function<void(const std::string&)>& receiver)
142
{
143
    auto min_time_opt = this->get_min_row_time();
294✔
144
    if (min_time_opt) {
294✔
145
        auto min_time_str = lnav::to_rfc3339_string(min_time_opt.value(), 'T');
3✔
146
        receiver(fmt::format(FMT_STRING("hide-lines-before {}"), min_time_str));
12✔
147
    }
3✔
148
    auto max_time_opt = this->get_max_row_time();
294✔
149
    if (max_time_opt) {
294✔
150
        auto max_time_str = lnav::to_rfc3339_string(max_time_opt.value(), 'T');
×
151
        receiver(fmt::format(FMT_STRING("hide-lines-after {}"), max_time_str));
×
152
    }
153
}
294✔
154

155
log_accel::direction_t
156
text_accel_source::get_line_accel_direction(vis_line_t vl)
211✔
157
{
158
    log_accel la;
211✔
159

160
    while (vl >= 0) {
1,864✔
161
        const auto* curr_line = this->text_accel_get_line(vl);
1,855✔
162

163
        if (!curr_line->is_message()) {
1,855✔
164
            --vl;
×
165
            continue;
×
166
        }
167

168
        if (!la.add_point(
1,855✔
169
                curr_line->get_time<std::chrono::milliseconds>().count()))
3,710✔
170
        {
171
            break;
202✔
172
        }
173

174
        --vl;
1,653✔
175
    }
176

177
    return la.get_direction();
422✔
178
}
179

180
std::string
181
text_accel_source::get_time_offset_for_line(textview_curses& tc, vis_line_t vl)
211✔
182
{
183
    auto ll = this->text_accel_get_line(vl);
211✔
184
    auto curr_tv = ll->get_timeval();
211✔
185
    timeval diff_tv;
186

187
    auto prev_umark = tc.get_bookmarks()[&textview_curses::BM_USER].prev(vl);
211✔
188
    auto next_umark = tc.get_bookmarks()[&textview_curses::BM_USER].next(vl);
211✔
189
    auto prev_emark
190
        = tc.get_bookmarks()[&textview_curses::BM_USER_EXPR].prev(vl);
211✔
191
    auto next_emark
192
        = tc.get_bookmarks()[&textview_curses::BM_USER_EXPR].next(vl);
211✔
193
    if (!prev_umark && !prev_emark && (next_umark || next_emark)) {
211✔
194
        auto next_line = this->text_accel_get_line(
×
195
            std::max(next_umark.value_or(0_vl), next_emark.value_or(0_vl)));
×
196

197
        diff_tv = curr_tv - next_line->get_timeval();
×
198
    } else {
199
        auto prev_row
200
            = std::max(prev_umark.value_or(0_vl), prev_emark.value_or(0_vl));
211✔
201
        auto* first_line = this->text_accel_get_line(prev_row);
211✔
202
        auto start_tv = first_line->get_timeval();
211✔
203
        diff_tv = curr_tv - start_tv;
211✔
204
    }
205

206
    return humanize::time::duration::from_tv(diff_tv).to_string();
211✔
207
}
208

209
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_ERRORS("error");
210
const DIST_SLICE(bm_types)
211
    bookmark_type_t textview_curses::BM_WARNINGS("warning");
212
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_USER("user");
213
const DIST_SLICE(bm_types)
214
    bookmark_type_t textview_curses::BM_USER_EXPR("user-expr");
215
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_SEARCH("search");
216
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_META("meta");
217
const DIST_SLICE(bm_types)
218
    bookmark_type_t textview_curses::BM_PARTITION("partition");
219
const DIST_SLICE(bm_types) bookmark_type_t textview_curses::BM_STICKY("sticky");
220

221
textview_curses::textview_curses()
24,228✔
222
    : lnav_config_listener(__FILE__), tc_search_action(noop_func{})
24,228✔
223
{
224
    this->set_data_source(this);
24,228✔
225
}
24,228✔
226

227
textview_curses::~textview_curses()
24,228✔
228
{
229
    this->tc_search_action = noop_func{};
24,228✔
230
}
24,228✔
231

232
void
233
textview_curses::reload_config(error_reporter& reporter)
14,064✔
234
{
235
    const static auto DEFAULT_THEME_NAME = std::string("default");
15,420✔
236
    const auto& vc = view_colors::singleton();
14,064✔
237

238
    for (auto iter = this->tc_highlights.begin();
14,064✔
239
         iter != this->tc_highlights.end();)
17,041✔
240
    {
241
        if (iter->first.first != highlight_source_t::THEME) {
2,977✔
242
            ++iter;
1,081✔
243
            continue;
1,081✔
244
        }
245

246
        iter = this->tc_highlights.erase(iter);
1,896✔
247
    }
248

249
    for (const auto& theme_name : {DEFAULT_THEME_NAME, lnav_config.lc_ui_theme})
70,320✔
250
    {
251
        auto theme_iter = lnav_config.lc_ui_theme_defs.find(theme_name);
28,128✔
252

253
        if (theme_iter == lnav_config.lc_ui_theme_defs.end()) {
28,128✔
254
            continue;
42✔
255
        }
256

257
        auto vars = &theme_iter->second.lt_vars;
28,086✔
258
        for (const auto& hl_pair : theme_iter->second.lt_highlights) {
140,182✔
259
            if (hl_pair.second.hc_regex.pp_value == nullptr) {
112,096✔
260
                continue;
×
261
            }
262

263
            const auto& sc = hl_pair.second.hc_style;
112,096✔
264
            std::string fg_color, bg_color, errmsg;
112,096✔
265
            bool invalid = false;
112,096✔
266
            text_attrs attrs;
112,096✔
267

268
            auto fg1 = sc.sc_color;
112,096✔
269
            auto bg1 = sc.sc_background_color;
112,096✔
270
            shlex(fg1).eval(fg_color, scoped_resolver{vars});
112,096✔
271
            shlex(bg1).eval(bg_color, scoped_resolver{vars});
112,096✔
272

273
            attrs.ta_fg_color = vc.match_color(
112,096✔
274
                styling::color_unit::from_str(fg_color).unwrapOrElse(
224,192✔
275
                    [&](const auto& msg) {
×
276
                        reporter(&sc.sc_color,
×
277
                                 lnav::console::user_message::error(
278
                                     attr_line_t("invalid color -- ")
×
279
                                         .append_quoted(sc.sc_color))
×
280
                                     .with_reason(msg));
×
281
                        invalid = true;
×
282
                        return styling::color_unit::EMPTY;
×
283
                    }));
284
            attrs.ta_bg_color = vc.match_color(
112,096✔
285
                styling::color_unit::from_str(bg_color).unwrapOrElse(
224,192✔
286
                    [&](const auto& msg) {
×
287
                        reporter(&sc.sc_background_color,
×
288
                                 lnav::console::user_message::error(
289
                                     attr_line_t("invalid background color -- ")
×
290
                                         .append_quoted(sc.sc_background_color))
×
291
                                     .with_reason(msg));
×
292
                        invalid = true;
×
293
                        return styling::color_unit::EMPTY;
×
294
                    }));
295
            if (invalid) {
112,096✔
296
                continue;
×
297
            }
298

299
            if (sc.sc_bold) {
112,096✔
300
                attrs |= text_attrs::style::bold;
×
301
            }
302
            if (sc.sc_underline) {
112,096✔
303
                attrs |= text_attrs::style::underline;
×
304
            }
305
            this->tc_highlights[{highlight_source_t::THEME, hl_pair.first}]
224,192✔
306
                = highlighter(hl_pair.second.hc_regex.pp_value)
224,192✔
307
                      .with_attrs(attrs)
112,096✔
308
                      .with_nestable(hl_pair.second.hc_nestable);
224,192✔
309
        }
112,096✔
310
    }
42,192✔
311

312
    if (this->tc_reload_config_delegate) {
14,064✔
313
        this->tc_reload_config_delegate(*this);
138✔
314
    }
315
}
14,064✔
316

317
void
318
textview_curses::invoke_scroll()
44,081✔
319
{
320
    this->tc_selected_text = std::nullopt;
44,081✔
321
    if (this->tc_sub_source != nullptr) {
44,081✔
322
        this->tc_sub_source->scroll_invoked(this);
19,853✔
323
    }
324

325
    listview_curses::invoke_scroll();
44,081✔
326
}
44,081✔
327

328
void
329
textview_curses::reload_data()
54,945✔
330
{
331
    this->tc_selected_text = std::nullopt;
54,945✔
332
    if (this->tc_sub_source != nullptr) {
54,945✔
333
        this->tc_sub_source->text_update_marks(this->tc_bookmarks);
19,608✔
334

335
        auto* ttt = dynamic_cast<text_time_translator*>(this->tc_sub_source);
19,608✔
336

337
        if (ttt != nullptr) {
19,608✔
338
            ttt->data_reloaded(this);
12,470✔
339
        }
340
        listview_curses::reload_data();
19,608✔
341
    }
342
}
54,945✔
343

344
void
345
textview_curses::grep_begin(grep_proc<vis_line_t>& gp,
46✔
346
                            vis_line_t start,
347
                            vis_line_t stop)
348
{
349
    require(this->tc_searching >= 0);
46✔
350

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

353
    this->tc_searching += 1;
46✔
354
    this->tc_search_action(this);
46✔
355

356
    if (start != -1_vl) {
46✔
357
        auto& search_bv = this->tc_bookmarks[&BM_SEARCH];
34✔
358
        auto pair = search_bv.equal_range(start, stop);
34✔
359

360
        if (pair.first != pair.second) {
34✔
361
            this->set_needs_update();
1✔
362
        }
363
        for (auto mark_iter = pair.first; mark_iter != pair.second; ++mark_iter)
35✔
364
        {
365
            if (this->tc_sub_source) {
1✔
366
                this->tc_sub_source->text_mark(&BM_SEARCH, *mark_iter, false);
1✔
367
            }
368
        }
369
        if (pair.first != pair.second) {
34✔
370
            auto to_del = std::vector<vis_line_t>{};
1✔
371
            for (auto file_iter = pair.first; file_iter != pair.second;
2✔
372
                 ++file_iter)
1✔
373
            {
374
                to_del.emplace_back(*file_iter);
1✔
375
            }
376

377
            for (auto cl : to_del) {
2✔
378
                search_bv.bv_tree.erase(cl);
1✔
379
            }
380
        }
1✔
381
    }
382

383
    listview_curses::reload_data();
46✔
384
}
46✔
385

386
void
387
textview_curses::grep_end_batch(grep_proc<vis_line_t>& gp)
64✔
388
{
389
    auto op_guard = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
64✔
390

391
    if (this->tc_follow_deadline.tv_sec
128✔
392
        && this->tc_follow_selection == this->get_selection())
64✔
393
    {
394
        timeval now;
395

396
        gettimeofday(&now, nullptr);
×
397
        if (this->tc_follow_deadline < now) {
×
398
        } else {
399
            if (this->tc_follow_func) {
×
400
                if (this->tc_follow_func()) {
×
401
                    this->tc_follow_deadline = {0, 0};
×
402
                }
403
            } else {
404
                this->tc_follow_deadline = {0, 0};
×
405
            }
406
        }
407
    }
408
    this->tc_search_action(this);
64✔
409
}
64✔
410

411
void
412
textview_curses::grep_end(grep_proc<vis_line_t>& gp)
46✔
413
{
414
    auto op_guard = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
46✔
415

416
    this->tc_searching -= 1;
46✔
417
    this->grep_end_batch(gp);
46✔
418
    if (this->tc_searching == 0 && this->tc_search_start_time) {
46✔
419
        const auto now = std::chrono::steady_clock::now();
9✔
420
        this->tc_search_duration
421
            = std::chrono::duration_cast<std::chrono::milliseconds>(
9✔
422
                now - this->tc_search_start_time.value());
18✔
423
        this->tc_search_start_time = std::nullopt;
9✔
424
        if (this->tc_state_event_handler) {
9✔
425
            this->tc_state_event_handler(*this);
×
426
        }
427
    }
428

429
    ensure(this->tc_searching >= 0);
46✔
430
}
46✔
431

432
void
433
textview_curses::grep_match(grep_proc<vis_line_t>& gp, vis_line_t line)
13✔
434
{
435
    this->tc_bookmarks[&BM_SEARCH].insert_once(vis_line_t(line));
13✔
436
    if (this->tc_sub_source != nullptr) {
13✔
437
        this->tc_sub_source->text_mark(&BM_SEARCH, line, true);
13✔
438
    }
439

440
    if (this->get_top() <= line && line <= this->get_bottom()) {
13✔
441
        listview_curses::reload_data();
13✔
442
    }
443
}
13✔
444

445
void
446
textview_curses::listview_value_for_rows(const listview_curses& lv,
13,549✔
447
                                         vis_line_t row,
448
                                         std::vector<attr_line_t>& rows_out)
449
{
450
    for (auto& al : rows_out) {
27,215✔
451
        this->textview_value_for_row(row, al);
13,666✔
452

453
        auto& sa = al.al_attrs;
13,666✔
454
        if (this->is_selectable() && this->tc_cursor_role
14,240✔
455
            && this->tc_disabled_cursor_role)
14,240✔
456
        {
457
            auto sel_end = this->get_selection().value_or(0_vl);
133✔
458
            auto sel_start = sel_end;
133✔
459
            if (this->tc_selection_start) {
133✔
460
                if (this->tc_selection_start.value() < sel_end) {
×
461
                    sel_start = this->tc_selection_start.value();
×
462
                } else {
463
                    sel_end = this->tc_selection_start.value();
×
464
                }
465
            }
466

467
            if (sel_start <= row && row <= sel_end) {
133✔
468
                auto role = (this->get_overlay_selection() || !this->vc_enabled)
26✔
469
                    ? this->tc_disabled_cursor_role.value()
19✔
470
                    : this->tc_cursor_role.value();
7✔
471

472
                sa.emplace_back(line_range{0, -1}, VC_ROLE.value(role));
13✔
473
            }
474
        }
475

476
        if (this->tc_selected_text) {
13,666✔
477
            const auto& sti = this->tc_selected_text.value();
×
478
            if (sti.sti_line == row) {
×
479
                sa.emplace_back(sti.sti_range,
×
480
                                VC_ROLE.value(role_t::VCR_SELECTED_TEXT));
×
481
            }
482
        }
483

484
        ++row;
13,666✔
485
    }
486
}
13,549✔
487

488
bool
489
textview_curses::handle_mouse(mouse_event& me)
×
490
{
491
    if (!this->vc_visible || this->lv_height == 0) {
×
492
        return false;
×
493
    }
494

495
    if (!this->tc_selection_start && listview_curses::handle_mouse(me)) {
×
496
        return true;
×
497
    }
498

499
    auto mouse_line = (me.me_y < 0 || me.me_y >= this->lv_display_lines.size())
×
500
        ? empty_space{}
×
501
        : this->lv_display_lines[me.me_y];
×
502
    auto [height, width] = this->get_dimensions();
×
503

504
    if (!mouse_line.is<overlay_menu>()
×
505
        && (me.me_button != mouse_button_t::BUTTON_LEFT
×
506
            || me.me_state != mouse_button_state_t::BUTTON_STATE_RELEASED))
×
507
    {
508
        this->tc_selected_text = std::nullopt;
×
509
        this->set_needs_update();
×
510
    }
511

512
    std::optional<int> overlay_content_min_y;
×
513
    std::optional<int> overlay_content_max_y;
×
514
    if (this->tc_press_line.is<overlay_content>()) {
×
515
        auto main_line
516
            = this->tc_press_line.get<overlay_content>().oc_main_line;
×
517
        for (size_t lpc = 0; lpc < this->lv_display_lines.size(); lpc++) {
×
518
            if (overlay_content_min_y
×
519
                && !this->lv_display_lines[lpc].is<static_overlay_content>()
×
520
                && !this->lv_display_lines[lpc].is<overlay_content>())
×
521
            {
522
                overlay_content_max_y = lpc;
×
523
                break;
×
524
            }
525
            if (this->lv_display_lines[lpc].is<main_content>()) {
×
526
                auto& mc = this->lv_display_lines[lpc].get<main_content>();
×
527
                if (mc.mc_line == main_line) {
×
528
                    overlay_content_min_y = lpc;
×
529
                }
530
            }
531
        }
532
        if (overlay_content_min_y && !overlay_content_max_y) {
×
533
            overlay_content_max_y = this->lv_display_lines.size();
×
534
        }
535
    }
536

537
    auto* sub_delegate = dynamic_cast<text_delegate*>(this->tc_sub_source);
×
538

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

595
                            this->textview_value_for_row(mc.mc_line, al);
×
596
                            auto line_sf
597
                                = string_fragment::from_str(al.get_string());
×
598
                            auto cursor_sf = line_sf.sub_cell_range(
×
599
                                mc.mc_line_range.lr_start + me.me_x,
×
600
                                mc.mc_line_range.lr_start + me.me_x);
×
601
                            auto ds = data_scanner(line_sf);
×
602
                            auto tf = this->tc_sub_source->get_text_format();
×
603
                            while (true) {
604
                                auto tok_res = ds.tokenize2(
×
605
                                    tf.value_or(text_format_t::TF_PLAINTEXT));
×
606
                                if (!tok_res) {
×
607
                                    break;
×
608
                                }
609

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

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

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

783
                this->textview_value_for_row(mc.mc_line, al);
×
784
                auto line_sf = string_fragment::from_str(al.get_string());
×
785
                auto cursor_sf = line_sf.sub_cell_range(
×
786
                    mc.mc_line_range.lr_start + me.me_x,
×
787
                    mc.mc_line_range.lr_start + me.me_x);
×
788
                auto link_iter = find_string_attr_containing(
×
789
                    al.get_attrs(), &VC_HYPERLINK, cursor_sf.sf_begin);
×
790
                if (link_iter != al.get_attrs().end()) {
×
791
                    auto href = link_iter->sa_value.get<std::string>();
×
792
                    auto* ta = dynamic_cast<text_anchors*>(this->tc_sub_source);
×
793

794
                    if (me.me_button == mouse_button_t::BUTTON_LEFT
×
795
                        && ta != nullptr && startswith(href, "#")
×
796
                        && !startswith(href, "#/frontmatter"))
×
797
                    {
798
                        auto row_opt = ta->row_for_anchor(href);
×
799

800
                        if (row_opt.has_value()) {
×
801
                            this->set_selection(row_opt.value());
×
802
                        }
803
                    } else {
804
                        this->tc_selected_text = selected_text_info{
×
805
                            me.me_x,
×
806
                            mc.mc_line,
×
807
                            link_iter->sa_range,
×
808
                            al.get_attrs(),
×
809
                            al.to_string_fragment(link_iter).to_string(),
×
810
                            href,
811
                        };
812
                        this->set_needs_update();
×
813
                    }
814
                }
815
                if (this->tc_on_click) {
×
816
                    this->tc_on_click(*this, al, cursor_sf.sf_begin);
×
817
                }
818
            }
819
            if (mouse_line.is<overlay_content>()) {
×
820
                const auto& oc = mouse_line.get<overlay_content>();
×
821
                std::vector<attr_line_t> ov_lines;
×
822

823
                this->lv_overlay_source->list_value_for_overlay(
×
824
                    *this, oc.oc_main_line, ov_lines);
825
                const auto& al = ov_lines[oc.oc_line];
×
826
                auto line_sf = string_fragment::from_str(al.get_string());
×
827
                auto cursor_sf = line_sf.sub_cell_range(
×
828
                    this->lv_left + me.me_x, this->lv_left + me.me_x);
×
829
                auto link_iter = find_string_attr_containing(
×
830
                    al.get_attrs(), &VC_HYPERLINK, cursor_sf.sf_begin);
831
                if (link_iter != al.get_attrs().end()) {
×
832
                    auto href = link_iter->sa_value.get<std::string>();
×
833
                    auto* ta = dynamic_cast<text_anchors*>(this->tc_sub_source);
×
834

835
                    if (me.me_button == mouse_button_t::BUTTON_LEFT
×
836
                        && ta != nullptr && startswith(href, "#")
×
837
                        && !startswith(href, "#/frontmatter"))
×
838
                    {
839
                        auto row_opt = ta->row_for_anchor(href);
×
840

841
                        if (row_opt.has_value()) {
×
842
                            this->tc_sub_source->get_location_history() |
×
843
                                [&oc](auto lh) {
×
844
                                    lh->loc_history_append(oc.oc_main_line);
×
845
                                };
846
                            this->set_selection(row_opt.value());
×
847
                        }
848
                    }
849
                }
850
                if (this->tc_on_click) {
×
851
                    this->tc_on_click(*this, al, cursor_sf.sf_begin);
×
852
                }
853
            }
854
            if (this->tc_delegate != nullptr) {
×
855
                this->tc_delegate->text_handle_mouse(*this, mouse_line, me);
×
856
            }
857
            if (sub_delegate != nullptr) {
×
858
                sub_delegate->text_handle_mouse(*this, mouse_line, me);
×
859
            }
860
            if (mouse_line.is<overlay_menu>()) {
×
861
                this->tc_selected_text = std::nullopt;
×
862
                this->set_needs_update();
×
863
            }
864
            break;
×
865
        }
866
    }
867

868
    return true;
×
869
}
870

871
void
872
textview_curses::apply_highlights(attr_line_t& al,
13,186✔
873
                                  const line_range& body,
874
                                  const line_range& orig_line)
875
{
876
    auto source_format = this->tc_sub_source->get_text_format();
13,186✔
877
    if (source_format.value_or(text_format_t::TF_BINARY)
13,186✔
878
        == text_format_t::TF_BINARY)
13,186✔
879
    {
880
        return;
4,803✔
881
    }
882
    for (const auto& tc_highlight : this->tc_highlights) {
125,024✔
883
        bool internal_hl
116,641✔
884
            = tc_highlight.first.first == highlight_source_t::INTERNAL
116,641✔
885
            || tc_highlight.first.first == highlight_source_t::THEME;
116,641✔
886

887
        if (!tc_highlight.second.applies_to_format(source_format.value())) {
116,641✔
888
            continue;
49,402✔
889
        }
890

891
        if (this->tc_disabled_highlights.is_set(tc_highlight.first.first)) {
67,243✔
892
            continue;
4✔
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;
67,239✔
901
        tc_highlight.second.annotate(al, lr);
67,239✔
902
    }
903
}
904

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

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

914
    for (const auto& attr : sa) {
93,883✔
915
        require_ge(attr.sa_range.lr_start, 0);
79,755✔
916
    }
917

918
    auto body = find_string_attr_range(sa, &SA_BODY);
14,128✔
919
    if (!body.is_valid()) {
14,128✔
920
        body.lr_start = 0;
10,917✔
921
        body.lr_end = str.size();
10,917✔
922
    }
923

924
    auto orig_line = find_string_attr_range(sa, &SA_ORIGINAL_LINE);
14,128✔
925
    if (!orig_line.is_valid()) {
14,128✔
926
        orig_line.lr_start = 0;
8,822✔
927
        orig_line.lr_end = str.size();
8,822✔
928
    }
929

930
    if (!body.empty() || !orig_line.empty()) {
14,128✔
931
        this->apply_highlights(value_out, body, orig_line);
12,100✔
932
    }
933

934
    if (this->tc_hide_fields) {
14,128✔
935
        value_out.apply_hide();
14,128✔
936
    }
937

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

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

954
    std::string regex = regex_orig;
27✔
955

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

963
        this->tc_search_op_id = std::nullopt;
27✔
964
        this->tc_search_child.reset();
27✔
965
        this->tc_source_search_child.reset();
27✔
966

967
        log_debug("%s: start search for: '%s'",
27✔
968
                  this->vc_title.c_str(),
969
                  regex.c_str());
970

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

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

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

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

996
        if (code != nullptr) {
27✔
997
            highlighter hl(code);
9✔
998

999
            hl.with_role(role_t::VCR_SEARCH);
9✔
1000

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

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

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

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

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

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

1038
                    sgp->set_sink(pair.second);
9✔
1039
                    sgp->queue_request(
18✔
1040
                        0_vl,
1041
                        sgp->until_eof(this->tc_sub_source->text_line_count()));
9✔
1042
                    sgp->start();
9✔
1043

1044
                    this->tc_source_search_child = sgp;
9✔
1045
                };
9✔
1046
            }
1047
        }
9✔
1048
    }
27✔
1049

1050
    this->tc_current_search = regex;
27✔
1051
    if (this->tc_state_event_handler) {
27✔
1052
        this->tc_state_event_handler(*this);
1✔
1053
    }
1054
}
27✔
1055

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

1068
    for (; start < end; ++start) {
×
1069
        std::vector<attr_line_t> rows(1);
×
1070
        this->listview_value_for_rows(*this, start, rows);
×
1071

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

1084
    if (prev_hit == -1 && next_hit == INT_MAX) {
×
1085
        return std::nullopt;
×
1086
    }
1087
    return std::make_pair(prev_hit, next_hit);
×
1088
}
1089

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

1097
    if (marked) {
201✔
1098
        bv.insert_once(vl);
47✔
1099
    } else {
1100
        bv.erase(vl);
154✔
1101
    }
1102
    if (this->tc_sub_source) {
201✔
1103
        this->tc_sub_source->text_mark(bm, vl, marked);
201✔
1104
    }
1105

1106
    if (marked) {
201✔
1107
        this->search_range(vl, grep_proc<vis_line_t>::until_line(vl + 1_vl));
47✔
1108
        this->search_new_data();
47✔
1109
    }
1110
    this->set_needs_update();
201✔
1111
}
201✔
1112

1113
textview_curses::mark_toggle_result
1114
textview_curses::toggle_user_mark(const bookmark_type_t* bm,
28✔
1115
                                  vis_line_t start_line,
1116
                                  vis_line_t end_line)
1117
{
1118
    mark_toggle_result retval;
28✔
1119

1120
    if (end_line == -1) {
28✔
1121
        end_line = start_line;
28✔
1122
    }
1123
    if (start_line > end_line) {
28✔
1124
        std::swap(start_line, end_line);
×
1125
    }
1126

1127
    if (start_line >= this->get_inner_height()) {
28✔
1128
        return retval;
×
1129
    }
1130
    if (end_line >= this->get_inner_height()) {
28✔
1131
        end_line = vis_line_t(this->get_inner_height() - 1);
×
1132
    }
1133
    for (auto curr_line = start_line; curr_line <= end_line; ++curr_line) {
56✔
1134
        auto& bv = this->tc_bookmarks[bm];
28✔
1135
        auto [insert_iter, added] = bv.insert_once(curr_line);
28✔
1136
        if (!added) {
28✔
1137
            bv.erase(curr_line);
3✔
1138
            retval.mtr_unmarked += 1;
3✔
1139
        } else {
1140
            retval.mtr_marked += 1;
25✔
1141
        }
1142
        if (this->tc_sub_source) {
28✔
1143
            this->tc_sub_source->text_mark(bm, curr_line, added);
28✔
1144
        }
1145
    }
1146
    this->search_range(start_line,
28✔
1147
                       grep_proc<vis_line_t>::until_line(end_line + 1_vl));
28✔
1148
    this->search_new_data();
28✔
1149

1150
    return retval;
28✔
1151
}
1152

1153
void
1154
textview_curses::redo_search()
1,440✔
1155
{
1156
    if (this->tc_search_child) {
1,440✔
1157
        auto op_guard
1158
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
3✔
1159

1160
        auto* gp = this->tc_search_child->get_grep_proc();
3✔
1161

1162
        gp->invalidate();
3✔
1163
        this->match_reset();
3✔
1164
        gp->queue_request(0_vl,
3✔
1165
                          gp->until_eof(this->tc_sub_source->text_line_count()))
3✔
1166
            .start();
6✔
1167

1168
        if (this->tc_source_search_child) {
3✔
1169
            this->tc_source_search_child->invalidate()
3✔
1170
                .queue_request(0_vl,
3✔
1171
                               this->tc_source_search_child->until_eof(
9✔
1172
                                   this->tc_sub_source->text_line_count()))
3✔
1173
                .start();
3✔
1174
        }
1175
    }
3✔
1176
}
1,440✔
1177

1178
void
1179
textview_curses::search_range(vis_line_t start,
264✔
1180
                              grep_proc<vis_line_t>::request_until_t stop)
1181
{
1182
    if (this->tc_search_child) {
264✔
1183
        auto op_guard
1184
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
11✔
1185
        auto* gp = this->tc_search_child->get_grep_proc();
11✔
1186
        gp->queue_request(start, stop);
11✔
1187
    }
11✔
1188
    if (this->tc_source_search_child) {
264✔
1189
        auto op_guard
1190
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
11✔
1191
        this->tc_source_search_child->queue_request(start, stop);
11✔
1192
    }
11✔
1193
}
264✔
1194

1195
void
1196
textview_curses::search_new_data(vis_line_t start)
188✔
1197
{
1198
    if (this->tc_sub_source != nullptr) {
188✔
1199
        this->search_range(start,
188✔
1200
                           grep_proc<vis_line_t>::until_eof(
1201
                               this->tc_sub_source->text_line_count()));
188✔
1202
    }
1203
    if (this->tc_search_child) {
188✔
1204
        auto op_guard
1205
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
6✔
1206

1207
        this->tc_search_child->get_grep_proc()->start();
6✔
1208
    }
6✔
1209
    if (this->tc_source_search_child) {
188✔
1210
        auto op_guard
1211
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
6✔
1212

1213
        this->tc_source_search_child->start();
6✔
1214
    }
6✔
1215
}
188✔
1216

1217
bool
1218
textview_curses::listview_is_row_selectable(const listview_curses& lv,
32✔
1219
                                            vis_line_t row)
1220
{
1221
    if (this->tc_sub_source != nullptr) {
32✔
1222
        return this->tc_sub_source->text_is_row_selectable(*this, row);
32✔
1223
    }
1224

1225
    return true;
×
1226
}
1227

1228
void
1229
textview_curses::listview_selection_changed(const listview_curses& lv)
2,323✔
1230
{
1231
    if (this->tc_sub_source != nullptr) {
2,323✔
1232
        this->tc_sub_source->text_selection_changed(*this);
2,323✔
1233
    }
1234
}
2,323✔
1235

1236
textview_curses&
1237
textview_curses::set_sub_source(text_sub_source* src)
17,458✔
1238
{
1239
    if (this->tc_sub_source != src) {
17,458✔
1240
        this->tc_bookmarks.clear();
14,229✔
1241
        this->tc_sub_source = src;
14,229✔
1242
        if (src) {
14,229✔
1243
            src->register_view(this);
9,501✔
1244
        }
1245
        this->reload_data();
14,229✔
1246
    }
1247
    return *this;
17,458✔
1248
}
1249

1250
std::optional<line_info>
1251
textview_curses::grep_value_for_line(vis_line_t line, std::string& value_out)
3✔
1252
{
1253
    // log_debug("grep line %d", line);
1254
    if (this->tc_sub_source
6✔
1255
        && line < (int) this->tc_sub_source->text_line_count())
3✔
1256
    {
1257
        auto retval = this->tc_sub_source->text_value_for_line(
3✔
1258
            *this, line, value_out, text_sub_source::RF_RAW);
1259
        if (retval.li_utf8_scan_result.is_valid()
3✔
1260
            && retval.li_utf8_scan_result.usr_has_ansi)
3✔
1261
        {
1262
            // log_debug("has ansi %d",
1263
            // retval.li_utf8_scan_result.usr_has_ansi);
1264
            auto new_size = erase_ansi_escapes(value_out);
×
1265
            value_out.resize(new_size);
×
1266
        }
1267
        // log_debug("  line off %lld", retval.li_file_range.fr_offset);
1268
        return retval;
3✔
1269
    }
1270

1271
    return std::nullopt;
×
1272
}
1273

1274
void
1275
textview_curses::update_hash_state(hasher& h) const
18✔
1276
{
1277
    listview_curses::update_hash_state(h);
18✔
1278

1279
    if (this->tc_sub_source != nullptr) {
18✔
1280
        this->tc_sub_source->update_filter_hash_state(h);
18✔
1281
    }
1282
}
18✔
1283

1284
void
1285
textview_curses::clear_preview()
1✔
1286
{
1287
    auto& hl = this->get_highlights();
1✔
1288
    hl.erase({highlight_source_t::PREVIEW, "preview"});
1✔
1289
    hl.erase({highlight_source_t::PREVIEW, "bodypreview"});
1✔
1290
    if (this->tc_sub_source != nullptr) {
1✔
1291
        this->tc_sub_source->clear_preview();
1✔
1292
    }
1293
}
1✔
1294

1295
void
1296
text_sub_source::scroll_invoked(textview_curses* tc)
19,036✔
1297
{
1298
    auto* ttt = dynamic_cast<text_time_translator*>(this);
19,036✔
1299

1300
    if (ttt != nullptr) {
19,036✔
1301
        ttt->ttt_scroll_invoked(tc);
11,889✔
1302
    }
1303
}
19,036✔
1304

1305
void
1306
text_sub_source::clear_preview()
1✔
1307
{
1308
    auto* ttt = dynamic_cast<text_time_translator*>(this);
1✔
1309

1310
    if (ttt != nullptr) {
1✔
1311
        ttt->clear_preview_times();
1✔
1312
    }
1313
    this->tss_preview_min_log_level = std::nullopt;
1✔
1314
}
1✔
1315

1316
void
1317
text_sub_source::add_commands_for_session(
343✔
1318
    const std::function<void(const std::string&)>& receiver)
1319
{
1320
    for (const auto& filter : this->tss_filters) {
349✔
1321
        receiver(filter->to_command());
6✔
1322

1323
        if (!filter->is_enabled()) {
6✔
1324
            receiver(
4✔
1325
                fmt::format(FMT_STRING("disable-filter {}"), filter->get_id()));
16✔
1326
        }
1327
    }
1328

1329
    auto* ttt = dynamic_cast<text_time_translator*>(this);
343✔
1330
    if (ttt != nullptr) {
343✔
1331
        ttt->add_time_commands_for_session(receiver);
294✔
1332
    }
1333

1334
    const auto& hmap = this->tss_view->get_highlights();
343✔
1335
    for (const auto& hl : hmap) {
4,159✔
1336
        if (hl.first.first != highlight_source_t::INTERACTIVE) {
3,816✔
1337
            continue;
3,816✔
1338
        }
1339
        receiver(fmt::format(FMT_STRING("highlight {}"), hl.first.second));
×
1340
    }
1341
}
343✔
1342

1343
void
1344
text_time_translator::ttt_scroll_invoked(textview_curses* tc)
11,889✔
1345
{
1346
    if (tc->get_inner_height() > 0 && tc->get_selection()) {
11,889✔
1347
        this->time_for_row(tc->get_selection().value()) |
1,935✔
1348
            [this](auto new_top_ri) { this->ttt_top_row_info = new_top_ri; };
1,686✔
1349
    }
1350
}
11,889✔
1351

1352
void
1353
text_time_translator::data_reloaded(textview_curses* tc)
12,470✔
1354
{
1355
    if (tc->get_inner_height() == 0) {
12,470✔
1356
        this->ttt_top_row_info = std::nullopt;
10,602✔
1357
        return;
10,602✔
1358
    }
1359
    if (this->ttt_top_row_info) {
1,868✔
1360
        this->row_for(this->ttt_top_row_info.value()) |
436✔
1361
            [tc](auto new_top) { tc->set_selection(new_top); };
409✔
1362
    }
1363
}
1364

1365
template class bookmark_vector<vis_line_t>;
1366

1367
bool
1368
empty_filter::matches(std::optional<line_source> ls,
×
1369
                      const shared_buffer_ref& line)
1370
{
1371
    return false;
×
1372
}
1373

1374
std::string
1375
empty_filter::to_command() const
×
1376
{
1377
    return "";
×
1378
}
1379

1380
bool
1381
pcre_filter::matches(std::optional<line_source> ls,
1,625✔
1382
                     const shared_buffer_ref& line)
1383
{
1384
    auto options = 0;
1,625✔
1385
    if (line.get_metadata().m_valid_utf) {
1,625✔
1386
        options |= PCRE2_NO_UTF_CHECK;
1,625✔
1387
    }
1388

1389
    return this->pf_pcre->find_in(line.to_string_fragment(), options)
3,250✔
1390
        .ignore_error()
3,250✔
1391
        .has_value();
3,250✔
1392
}
1393

1394
filter_stack::iterator
1395
filter_stack::find(size_t index)
810✔
1396
{
1397
    for (auto iter = this->fs_filters.begin(); iter != this->fs_filters.end();
898✔
1398
         ++iter)
88✔
1399
    {
1400
        if ((*iter)->get_index() == index) {
95✔
1401
            return iter;
7✔
1402
        }
1403
    }
1404

1405
    return this->fs_filters.end();
803✔
1406
}
1407

1408
std::optional<size_t>
1409
filter_stack::next_index()
98✔
1410
{
1411
    bool used[32];
1412

1413
    memset(used, 0, sizeof(used));
98✔
1414
    for (auto& iter : *this) {
102✔
1415
        if (iter->lf_deleted) {
4✔
1416
            continue;
×
1417
        }
1418

1419
        size_t index = iter->get_index();
4✔
1420

1421
        require(used[index] == false);
4✔
1422

1423
        used[index] = true;
4✔
1424
    }
1425
    for (size_t lpc = this->fs_reserved;
102✔
1426
         lpc < logfile_filter_state::MAX_FILTERS;
102✔
1427
         lpc++)
1428
    {
1429
        if (!used[lpc]) {
102✔
1430
            return lpc;
98✔
1431
        }
1432
    }
1433
    return std::nullopt;
×
1434
}
1435

1436
std::shared_ptr<text_filter>
1437
filter_stack::get_filter(const std::string& id)
41✔
1438
{
1439
    auto iter = this->fs_filters.begin();
41✔
1440
    std::shared_ptr<text_filter> retval;
41✔
1441

1442
    for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {}
47✔
1443
    if (iter != this->fs_filters.end()) {
41✔
1444
        retval = *iter;
4✔
1445
    }
1446

1447
    return retval;
82✔
1448
}
×
1449

1450
bool
1451
filter_stack::delete_filter(const std::string& id)
9✔
1452
{
1453
    auto iter = this->fs_filters.begin();
9✔
1454

1455
    for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {}
9✔
1456
    if (iter != this->fs_filters.end()) {
9✔
1457
        this->fs_filters.erase(iter);
9✔
1458
        this->fs_generation += 1;
9✔
1459
        return true;
9✔
1460
    }
1461

1462
    return false;
×
1463
}
1464

1465
void
1466
filter_stack::get_mask(uint32_t& filter_mask)
×
1467
{
1468
    filter_mask = 0;
×
1469
    for (auto& iter : *this) {
×
1470
        std::shared_ptr<text_filter> tf = iter;
×
1471

1472
        if (tf->lf_deleted) {
×
1473
            continue;
×
1474
        }
1475
        if (tf->is_enabled()) {
×
1476
            uint32_t bit = (1UL << tf->get_index());
×
1477

1478
            switch (tf->get_type()) {
×
1479
                case text_filter::EXCLUDE:
×
1480
                case text_filter::INCLUDE:
1481
                    filter_mask |= bit;
×
1482
                    break;
×
1483
                default:
×
1484
                    ensure(0);
×
1485
                    break;
1486
            }
1487
        }
1488
    }
1489
}
1490

1491
void
1492
filter_stack::get_enabled_mask(uint32_t& filter_in_mask,
1,964✔
1493
                               uint32_t& filter_out_mask)
1494
{
1495
    filter_in_mask = filter_out_mask = 0;
1,964✔
1496
    for (auto& iter : *this) {
2,235✔
1497
        std::shared_ptr<text_filter> tf = iter;
271✔
1498

1499
        if (tf->lf_deleted) {
271✔
1500
            continue;
6✔
1501
        }
1502
        if (tf->is_enabled()) {
265✔
1503
            uint32_t bit = (1UL << tf->get_index());
107✔
1504

1505
            switch (tf->get_type()) {
107✔
1506
                case text_filter::EXCLUDE:
56✔
1507
                    filter_out_mask |= bit;
56✔
1508
                    break;
56✔
1509
                case text_filter::INCLUDE:
51✔
1510
                    filter_in_mask |= bit;
51✔
1511
                    break;
51✔
1512
                default:
×
1513
                    ensure(0);
×
1514
                    break;
1515
            }
1516
        }
1517
    }
271✔
1518
}
1,964✔
1519

1520
void
1521
filter_stack::add_filter(const std::shared_ptr<text_filter>& filter)
102✔
1522
{
1523
    this->fs_filters.push_back(filter);
102✔
1524
    this->fs_generation += 1;
102✔
1525
}
102✔
1526

1527
void
1528
vis_location_history::loc_history_append(vis_line_t top)
37✔
1529
{
1530
    auto iter = this->vlh_history.begin();
37✔
1531
    iter += this->vlh_history.size() - this->lh_history_position;
37✔
1532
    this->vlh_history.erase_from(iter);
37✔
1533
    this->lh_history_position = 0;
37✔
1534
    this->vlh_history.push_back(top);
37✔
1535
}
37✔
1536

1537
std::optional<vis_line_t>
1538
vis_location_history::loc_history_back(vis_line_t current_top)
×
1539
{
1540
    if (this->lh_history_position == 0) {
×
1541
        vis_line_t history_top = this->current_position();
×
1542
        if (history_top != current_top) {
×
1543
            return history_top;
×
1544
        }
1545
    }
1546

1547
    if (this->lh_history_position + 1 >= this->vlh_history.size()) {
×
1548
        return std::nullopt;
×
1549
    }
1550

1551
    this->lh_history_position += 1;
×
1552

1553
    return this->current_position();
×
1554
}
1555

1556
std::optional<vis_line_t>
1557
vis_location_history::loc_history_forward(vis_line_t current_top)
×
1558
{
1559
    if (this->lh_history_position == 0) {
×
1560
        return std::nullopt;
×
1561
    }
1562

1563
    this->lh_history_position -= 1;
×
1564

1565
    return this->current_position();
×
1566
}
1567

1568
void
1569
text_sub_source::update_filter_hash_state(hasher& h) const
18✔
1570
{
1571
    h.update(this->tss_filters.fs_generation);
18✔
1572

1573
    const auto* ttt = dynamic_cast<const text_time_translator*>(this);
18✔
1574
    if (ttt != nullptr) {
18✔
1575
        auto min_time = ttt->get_min_row_time();
18✔
1576
        if (min_time) {
18✔
1577
            h.update(min_time->tv_sec);
×
1578
            h.update(min_time->tv_usec);
×
1579
        } else {
1580
            h.update(0);
18✔
1581
            h.update(0);
18✔
1582
        }
1583
        auto max_time = ttt->get_max_row_time();
18✔
1584
        if (max_time) {
18✔
1585
            h.update(max_time->tv_sec);
×
1586
            h.update(max_time->tv_usec);
×
1587
        } else {
1588
            h.update(0);
18✔
1589
            h.update(0);
18✔
1590
        }
1591
    }
1592
}
18✔
1593

1594
void
1595
text_sub_source::toggle_apply_filters()
1✔
1596
{
1597
    this->tss_apply_filters = !this->tss_apply_filters;
1✔
1598
    this->text_filters_changed();
1✔
1599
}
1✔
1600

1601
void
1602
text_sub_source::text_crumbs_for_line(int line,
43✔
1603
                                      std::vector<breadcrumb::crumb>& crumbs)
1604
{
1605
}
43✔
1606

1607
logfile_filter_state::logfile_filter_state(std::shared_ptr<logfile> lf)
1,210✔
1608
    : tfs_logfile(std::move(lf))
1,210✔
1609
{
1610
    memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
1,210✔
1611
    memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
1,210✔
1612
    memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
1,210✔
1613
    memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
1,210✔
1614
    memset(this->tfs_hits_for_message, 0, sizeof(this->tfs_hits_for_message));
1,210✔
1615
    memset(this->tfs_last_message_matched,
1,210✔
1616
           0,
1617
           sizeof(this->tfs_last_message_matched));
1618
    memset(this->tfs_last_lines_for_message,
1,210✔
1619
           0,
1620
           sizeof(this->tfs_last_lines_for_message));
1621
    this->tfs_mask.reserve(64 * 1024);
1,210✔
1622
}
1,210✔
1623

1624
void
1625
logfile_filter_state::clear()
551✔
1626
{
1627
    this->tfs_logfile = nullptr;
551✔
1628
    this->clear_for_rebuild();
551✔
1629
}
551✔
1630

1631
void
1632
logfile_filter_state::clear_for_rebuild()
556✔
1633
{
1634
    log_debug("clearing filter state");
556✔
1635
    memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
556✔
1636
    memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
556✔
1637
    memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
556✔
1638
    memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
556✔
1639
    memset(this->tfs_hits_for_message, 0, sizeof(this->tfs_hits_for_message));
556✔
1640
    memset(this->tfs_last_message_matched,
556✔
1641
           0,
1642
           sizeof(this->tfs_last_message_matched));
1643
    memset(this->tfs_last_lines_for_message,
556✔
1644
           0,
1645
           sizeof(this->tfs_last_lines_for_message));
1646
    this->tfs_mask.clear();
556✔
1647
    this->tfs_index.clear();
556✔
1648
}
556✔
1649

1650
void
1651
logfile_filter_state::clear_filter_state(size_t index)
4,078✔
1652
{
1653
    this->tfs_filter_count[index] = 0;
4,078✔
1654
    this->tfs_filter_hits[index] = 0;
4,078✔
1655
    this->tfs_message_matched[index] = false;
4,078✔
1656
    this->tfs_lines_for_message[index] = 0;
4,078✔
1657
    this->tfs_hits_for_message[index] = 0;
4,078✔
1658
    this->tfs_last_message_matched[index] = false;
4,078✔
1659
    this->tfs_last_lines_for_message[index] = 0;
4,078✔
1660
}
4,078✔
1661

1662
void
1663
logfile_filter_state::clear_deleted_filter_state(uint32_t used_mask)
128✔
1664
{
1665
    for (int lpc = 0; lpc < MAX_FILTERS; lpc++) {
4,224✔
1666
        if (!(used_mask & (1L << lpc))) {
4,096✔
1667
            this->clear_filter_state(lpc);
4,032✔
1668
        }
1669
    }
1670
    for (size_t lpc = 0; lpc < this->tfs_mask.size(); lpc++) {
1,636✔
1671
        this->tfs_mask[lpc] &= used_mask;
1,508✔
1672
    }
1673
}
128✔
1674

1675
void
1676
logfile_filter_state::resize(size_t newsize)
19,723✔
1677
{
1678
    size_t old_mask_size = this->tfs_mask.size();
19,723✔
1679

1680
    this->tfs_mask.resize(newsize);
19,723✔
1681
    if (newsize > old_mask_size) {
19,723✔
1682
        memset(&this->tfs_mask[old_mask_size],
18,710✔
1683
               0,
1684
               sizeof(uint32_t) * (newsize - old_mask_size));
18,710✔
1685
    }
1686
}
19,723✔
1687

1688
void
1689
logfile_filter_state::reserve(size_t expected)
2,497✔
1690
{
1691
    this->tfs_mask.reserve(expected);
2,497✔
1692
}
2,497✔
1693

1694
std::optional<size_t>
1695
logfile_filter_state::content_line_to_vis_line(uint32_t line)
×
1696
{
1697
    if (this->tfs_index.empty()) {
×
1698
        return std::nullopt;
×
1699
    }
1700

1701
    auto iter = std::lower_bound(
×
1702
        this->tfs_index.begin(), this->tfs_index.end(), line);
1703

1704
    if (iter == this->tfs_index.end() || *iter != line) {
×
1705
        return std::nullopt;
×
1706
    }
1707

1708
    return std::make_optional(std::distance(this->tfs_index.begin(), iter));
×
1709
}
1710

1711
std::string
1712
text_anchors::to_anchor_string(const std::string& raw)
63✔
1713
{
1714
    static const auto ANCHOR_RE = lnav::pcre2pp::code::from_const(R"([^\w]+)");
63✔
1715

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