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

tstack / lnav / 20245728190-2749

15 Dec 2025 07:59PM UTC coverage: 68.864% (-0.07%) from 68.929%
20245728190-2749

push

github

tstack
[text_format] add plaintext type

Related to #1296

85 of 132 new or added lines in 24 files covered. (64.39%)

73 existing lines in 10 files now uncovered.

51605 of 74938 relevant lines covered (68.86%)

434003.35 hits per line

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

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

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

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

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

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

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

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

96
    return retval;
1,349✔
97
}
98

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

241
    for (const auto& theme_name : {DEFAULT_THEME_NAME, lnav_config.lc_ui_theme})
66,520✔
242
    {
243
        auto theme_iter = lnav_config.lc_ui_theme_defs.find(theme_name);
26,608✔
244

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

249
        auto vars = &theme_iter->second.lt_vars;
26,566✔
250
        for (const auto& hl_pair : theme_iter->second.lt_highlights) {
132,582✔
251
            if (hl_pair.second.hc_regex.pp_value == nullptr) {
106,016✔
252
                continue;
×
253
            }
254

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

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

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

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

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

309
void
310
textview_curses::invoke_scroll()
42,302✔
311
{
312
    this->tc_selected_text = std::nullopt;
42,302✔
313
    if (this->tc_sub_source != nullptr) {
42,302✔
314
        this->tc_sub_source->scroll_invoked(this);
18,874✔
315
    }
316

317
    listview_curses::invoke_scroll();
42,302✔
318
}
42,302✔
319

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

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

329
        if (ttt != nullptr) {
18,663✔
330
            ttt->data_reloaded(this);
11,836✔
331
        }
332
        listview_curses::reload_data();
18,663✔
333
    }
334
}
52,650✔
335

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

437
void
438
textview_curses::listview_value_for_rows(const listview_curses& lv,
13,247✔
439
                                         vis_line_t row,
440
                                         std::vector<attr_line_t>& rows_out)
441
{
442
    for (auto& al : rows_out) {
26,596✔
443
        this->textview_value_for_row(row, al);
13,349✔
444

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

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

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

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

476
        ++row;
13,349✔
477
    }
478
}
13,247✔
479

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

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

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

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

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

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

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

587
                            this->textview_value_for_row(mc.mc_line, al);
×
588
                            auto line_sf
589
                                = string_fragment::from_str(al.get_string());
×
590
                            auto cursor_sf = line_sf.sub_cell_range(
×
591
                                mc.mc_line_range.lr_start + me.me_x,
×
592
                                mc.mc_line_range.lr_start + me.me_x);
×
593
                            auto ds = data_scanner(line_sf);
×
594
                            auto tf = this->tc_sub_source->get_text_format();
×
595
                            while (true) {
NEW
596
                                auto tok_res = ds.tokenize2(
×
NEW
597
                                    tf.value_or(text_format_t::TF_PLAINTEXT));
×
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
                                {
NEW
618
                                    auto group_tok = ds.find_matching_bracket(
×
619
                                        tf.value_or(
NEW
620
                                            text_format_t::TF_PLAINTEXT),
×
621
                                        tok);
622
                                    if (group_tok) {
×
623
                                        tok_sf = group_tok.value()
×
624
                                                     .to_string_fragment();
×
625
                                    }
626
                                    this->tc_selected_text = selected_text_info{
×
627
                                        me.me_x,
×
628
                                        mc.mc_line,
×
629
                                        line_range{
630
                                            tok_sf.sf_begin,
631
                                            tok_sf.sf_end,
632
                                        },
633
                                        al.al_attrs,
634
                                        tok_sf.to_string(),
635
                                    };
636
                                    this->set_needs_update();
×
637
                                    break;
×
638
                                }
639
                            }
640
                        }
641
                        this->set_selection_without_context(mc.mc_line);
×
642
                    }
643
                    if (this->tc_delegate != nullptr) {
×
644
                        this->tc_delegate->text_handle_mouse(
×
645
                            *this, mouse_line, me);
646
                    }
647
                    if (sub_delegate != nullptr) {
×
648
                        sub_delegate->text_handle_mouse(*this, mouse_line, me);
×
649
                    }
650
                },
×
651
                [](const static_overlay_content& soc) {},
×
652
                [](const overlay_menu& om) {},
×
653
                [](const overlay_content& oc) {},
×
654
                [](const empty_space& es) {});
×
655
            break;
×
656
        }
657
        case mouse_button_state_t::BUTTON_STATE_DRAGGED: {
×
658
            auto same_line = (this->tc_press_line.is<main_content>()
×
659
                              && mouse_line.is<main_content>()
×
660
                              && this->tc_press_line.get<main_content>().mc_line
×
661
                                  == mouse_line.get<main_content>().mc_line);
×
662
            this->tc_text_selection_active = true;
×
663
            if (!this->vc_enabled) {
×
664
            } else if (same_line) {
×
665
                const auto& mc = mouse_line.get<main_content>();
×
666
                attr_line_t al;
×
667
                auto low_x
668
                    = std::min(this->tc_press_left,
×
669
                               (int) mc.mc_line_range.lr_start + me.me_x);
×
670
                auto high_x
671
                    = std::max(this->tc_press_left,
×
672
                               (int) mc.mc_line_range.lr_start + me.me_x);
×
673

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

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

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

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

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

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

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

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

860
    return true;
×
861
}
862

863
void
864
textview_curses::apply_highlights(attr_line_t& al,
12,862✔
865
                                  const line_range& body,
866
                                  const line_range& orig_line)
867
{
868
    intern_string_t format_name;
12,862✔
869

870
    auto format_attr_opt = get_string_attr(al.al_attrs, SA_FORMAT);
12,862✔
871
    if (format_attr_opt.has_value()) {
12,862✔
872
        format_name = format_attr_opt.value().get();
4,488✔
873
    }
874

875
    auto source_format = this->tc_sub_source->get_text_format();
12,862✔
876
    if (source_format.value_or(text_format_t::TF_BINARY)
12,862✔
877
        == text_format_t::TF_BINARY)
12,862✔
878
    {
879
        return;
4,712✔
880
    }
881
    for (const auto& tc_highlight : this->tc_highlights) {
120,222✔
882
        bool internal_hl
112,072✔
883
            = tc_highlight.first.first == highlight_source_t::INTERNAL
112,072✔
884
            || tc_highlight.first.first == highlight_source_t::THEME;
112,072✔
885

886
        if (!tc_highlight.second.applies_to_format(source_format.value())) {
112,072✔
887
            continue;
46,328✔
888
        }
889

890
        if (!tc_highlight.second.h_format_name.empty()
65,748✔
891
            && tc_highlight.second.h_format_name != format_name)
65,748✔
892
        {
893
            continue;
×
894
        }
895

896
        if (this->tc_disabled_highlights.is_set(tc_highlight.first.first)) {
65,748✔
897
            continue;
4✔
898
        }
899

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

910
void
911
textview_curses::textview_value_for_row(vis_line_t row, attr_line_t& value_out)
13,776✔
912
{
913
    auto& sa = value_out.get_attrs();
13,776✔
914
    auto& str = value_out.get_string();
13,776✔
915

916
    this->tc_sub_source->text_value_for_line(*this, row, str);
13,776✔
917
    this->tc_sub_source->text_attrs_for_line(*this, row, sa);
13,776✔
918

919
    for (const auto& attr : sa) {
91,416✔
920
        require_ge(attr.sa_range.lr_start, 0);
77,640✔
921
    }
922

923
    line_range body, orig_line;
13,776✔
924

925
    body = find_string_attr_range(sa, &SA_BODY);
13,776✔
926
    if (!body.is_valid()) {
13,776✔
927
        body.lr_start = 0;
10,653✔
928
        body.lr_end = str.size();
10,653✔
929
    }
930

931
    orig_line = find_string_attr_range(sa, &SA_ORIGINAL_LINE);
13,776✔
932
    if (!orig_line.is_valid()) {
13,776✔
933
        orig_line.lr_start = 0;
8,594✔
934
        orig_line.lr_end = str.size();
8,594✔
935
    }
936

937
    if (!body.empty() || !orig_line.empty()) {
13,776✔
938
        this->apply_highlights(value_out, body, orig_line);
11,776✔
939
    }
940

941
    if (this->tc_hide_fields) {
13,776✔
942
        value_out.apply_hide();
13,776✔
943
    }
944

945
    const auto& user_marks = this->tc_bookmarks[&BM_USER];
13,776✔
946
    const auto& user_expr_marks = this->tc_bookmarks[&BM_USER_EXPR];
13,776✔
947
    if (this->tc_mark_style
13,776✔
948
        && (user_marks.bv_tree.exists(row)
27,534✔
949
            || user_expr_marks.bv_tree.exists(row)))
27,534✔
950
    {
951
        sa.emplace_back(line_range{orig_line.lr_start, -1},
35✔
952
                        this->tc_mark_style.value());
953
    }
954
}
13,776✔
955

956
void
957
textview_curses::execute_search(const std::string& regex_orig)
27✔
958
{
959
    static auto op = lnav_operation{"grep"};
27✔
960

961
    std::string regex = regex_orig;
27✔
962

963
    if ((this->tc_search_child == nullptr)
27✔
964
        || (regex != this->tc_current_search))
27✔
965
    {
966
        auto op_guard = lnav_opid_guard::async(op);
27✔
967
        std::shared_ptr<lnav::pcre2pp::code> code;
27✔
968
        this->match_reset();
27✔
969

970
        this->tc_search_op_id = std::nullopt;
27✔
971
        this->tc_search_child.reset();
27✔
972
        this->tc_source_search_child.reset();
27✔
973

974
        log_debug("%s: start search for: '%s'",
27✔
975
                  this->vc_title.c_str(),
976
                  regex.c_str());
977

978
        if (regex.empty()) {
27✔
979
        } else {
980
            auto compile_res = lnav::pcre2pp::code::from(regex, PCRE2_CASELESS);
9✔
981

982
            if (compile_res.isErr()) {
9✔
983
                auto ce = compile_res.unwrapErr();
×
984
                regex = lnav::pcre2pp::quote(regex);
×
985

986
                log_info("invalid search regex (%s), using quoted: %s",
×
987
                         ce.get_message().c_str(),
988
                         regex.c_str());
989

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

1003
        if (code != nullptr) {
27✔
1004
            highlighter hl(code);
9✔
1005

1006
            hl.with_role(role_t::VCR_SEARCH);
9✔
1007

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

1011
            auto gp = injector::get<std::shared_ptr<grep_proc<vis_line_t>>>(
1012
                code, *this);
9✔
1013

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

1029
            auto op_guard
1030
                = lnav_opid_guard::resume(this->tc_search_op_id.value());
9✔
1031
            gp->start();
9✔
1032

1033
            this->tc_search_child = std::make_shared<grep_highlighter>(
9✔
1034
                gp, highlight_source_t::PREVIEW, "search", hm);
9✔
1035

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

1042
                    sgp->set_sink(pair.second);
9✔
1043
                    sgp->queue_request(0_vl);
9✔
1044
                    sgp->start();
9✔
1045

1046
                    this->tc_source_search_child = sgp;
9✔
1047
                };
9✔
1048
            }
1049
        }
9✔
1050
    }
27✔
1051

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

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

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

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

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

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

1099
    if (marked) {
123✔
1100
        bv.insert_once(vl);
43✔
1101
    } else {
1102
        bv.erase(vl);
80✔
1103
    }
1104
    if (this->tc_sub_source) {
123✔
1105
        this->tc_sub_source->text_mark(bm, vl, marked);
123✔
1106
    }
1107

1108
    if (marked) {
123✔
1109
        this->search_range(vl, vl + 1_vl);
43✔
1110
        this->search_new_data();
43✔
1111
    }
1112
    this->set_needs_update();
123✔
1113
}
123✔
1114

1115
void
1116
textview_curses::toggle_user_mark(const bookmark_type_t* bm,
12✔
1117
                                  vis_line_t start_line,
1118
                                  vis_line_t end_line)
1119
{
1120
    if (end_line == -1) {
12✔
1121
        end_line = start_line;
12✔
1122
    }
1123
    if (start_line > end_line) {
12✔
1124
        std::swap(start_line, end_line);
×
1125
    }
1126

1127
    if (start_line >= this->get_inner_height()) {
12✔
1128
        return;
×
1129
    }
1130
    if (end_line >= this->get_inner_height()) {
12✔
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) {
24✔
1134
        auto& bv = this->tc_bookmarks[bm];
12✔
1135
        auto [insert_iter, added] = bv.insert_once(curr_line);
12✔
1136
        if (!added) {
12✔
1137
            bv.erase(curr_line);
1✔
1138
        }
1139
        if (this->tc_sub_source) {
12✔
1140
            this->tc_sub_source->text_mark(bm, curr_line, added);
12✔
1141
        }
1142
    }
1143
    this->search_range(start_line, end_line + 1_vl);
12✔
1144
    this->search_new_data();
12✔
1145
}
1146

1147
void
1148
textview_curses::redo_search()
1,351✔
1149
{
1150
    if (this->tc_search_child) {
1,351✔
1151
        auto op_guard
1152
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
3✔
1153

1154
        auto* gp = this->tc_search_child->get_grep_proc();
3✔
1155

1156
        gp->invalidate();
3✔
1157
        this->match_reset();
3✔
1158
        gp->queue_request(0_vl).start();
3✔
1159

1160
        if (this->tc_source_search_child) {
3✔
1161
            this->tc_source_search_child->invalidate()
3✔
1162
                .queue_request(0_vl)
6✔
1163
                .start();
3✔
1164
        }
1165
    }
3✔
1166
}
1,351✔
1167

1168
void
1169
textview_curses::search_range(vis_line_t start, vis_line_t stop)
216✔
1170
{
1171
    if (this->tc_search_child) {
216✔
1172
        auto op_guard
1173
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
11✔
1174
        this->tc_search_child->get_grep_proc()->queue_request(start, stop);
11✔
1175
    }
11✔
1176
    if (this->tc_source_search_child) {
216✔
1177
        auto op_guard
1178
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
11✔
1179
        this->tc_source_search_child->queue_request(start, stop);
11✔
1180
    }
11✔
1181
}
216✔
1182

1183
void
1184
textview_curses::search_new_data(vis_line_t start)
160✔
1185
{
1186
    this->search_range(start);
160✔
1187
    if (this->tc_search_child) {
160✔
1188
        auto op_guard
1189
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
6✔
1190

1191
        this->tc_search_child->get_grep_proc()->start();
6✔
1192
    }
6✔
1193
    if (this->tc_source_search_child) {
160✔
1194
        auto op_guard
1195
            = lnav_opid_guard::resume(this->tc_search_op_id.value_or(""));
6✔
1196

1197
        this->tc_source_search_child->start();
6✔
1198
    }
6✔
1199
}
160✔
1200

1201
bool
1202
textview_curses::listview_is_row_selectable(const listview_curses& lv,
23✔
1203
                                            vis_line_t row)
1204
{
1205
    if (this->tc_sub_source != nullptr) {
23✔
1206
        return this->tc_sub_source->text_is_row_selectable(*this, row);
23✔
1207
    }
1208

1209
    return true;
×
1210
}
1211

1212
void
1213
textview_curses::listview_selection_changed(const listview_curses& lv)
2,216✔
1214
{
1215
    if (this->tc_sub_source != nullptr) {
2,216✔
1216
        this->tc_sub_source->text_selection_changed(*this);
2,216✔
1217
    }
1218
}
2,216✔
1219

1220
textview_curses&
1221
textview_curses::set_sub_source(text_sub_source* src)
16,548✔
1222
{
1223
    if (this->tc_sub_source != src) {
16,548✔
1224
        this->tc_bookmarks.clear();
13,431✔
1225
        this->tc_sub_source = src;
13,431✔
1226
        if (src) {
13,431✔
1227
            src->register_view(this);
8,969✔
1228
        }
1229
        this->reload_data();
13,431✔
1230
    }
1231
    return *this;
16,548✔
1232
}
1233

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

1255
    return std::nullopt;
×
1256
}
1257

1258
void
1259
textview_curses::update_hash_state(hasher& h) const
16✔
1260
{
1261
    listview_curses::update_hash_state(h);
16✔
1262

1263
    if (this->tc_sub_source != nullptr) {
16✔
1264
        this->tc_sub_source->update_filter_hash_state(h);
16✔
1265
    }
1266
}
16✔
1267

1268
void
1269
textview_curses::clear_preview()
1✔
1270
{
1271
    auto& hl = this->get_highlights();
1✔
1272
    hl.erase({highlight_source_t::PREVIEW, "preview"});
1✔
1273
    hl.erase({highlight_source_t::PREVIEW, "bodypreview"});
1✔
1274
    if (this->tc_sub_source != nullptr) {
1✔
1275
        this->tc_sub_source->clear_preview();
1✔
1276
    }
1277
}
1✔
1278

1279
void
1280
text_sub_source::scroll_invoked(textview_curses* tc)
18,115✔
1281
{
1282
    auto* ttt = dynamic_cast<text_time_translator*>(this);
18,115✔
1283

1284
    if (ttt != nullptr) {
18,115✔
1285
        ttt->ttt_scroll_invoked(tc);
11,279✔
1286
    }
1287
}
18,115✔
1288

1289
void
1290
text_sub_source::clear_preview()
1✔
1291
{
1292
    auto* ttt = dynamic_cast<text_time_translator*>(this);
1✔
1293

1294
    if (ttt != nullptr) {
1✔
1295
        ttt->clear_preview_times();
1✔
1296
    }
1297
    this->tss_preview_min_log_level = std::nullopt;
1✔
1298
}
1✔
1299

1300
void
1301
text_sub_source::add_commands_for_session(
329✔
1302
    const std::function<void(const std::string&)>& receiver)
1303
{
1304
    for (const auto& filter : this->tss_filters) {
335✔
1305
        receiver(filter->to_command());
6✔
1306

1307
        if (!filter->is_enabled()) {
6✔
1308
            receiver(
4✔
1309
                fmt::format(FMT_STRING("disable-filter {}"), filter->get_id()));
16✔
1310
        }
1311
    }
1312

1313
    auto* ttt = dynamic_cast<text_time_translator*>(this);
329✔
1314
    if (ttt != nullptr) {
329✔
1315
        ttt->add_time_commands_for_session(receiver);
282✔
1316
    }
1317

1318
    const auto& hmap = this->tss_view->get_highlights();
329✔
1319
    for (const auto& hl : hmap) {
4,003✔
1320
        if (hl.first.first != highlight_source_t::INTERACTIVE) {
3,674✔
1321
            continue;
3,674✔
1322
        }
1323
        receiver(fmt::format(FMT_STRING("highlight {}"), hl.first.second));
×
1324
    }
1325
}
329✔
1326

1327
void
1328
text_time_translator::ttt_scroll_invoked(textview_curses* tc)
11,279✔
1329
{
1330
    if (tc->get_inner_height() > 0 && tc->get_selection()) {
11,279✔
1331
        this->time_for_row(tc->get_selection().value()) |
1,820✔
1332
            [this](auto new_top_ri) { this->ttt_top_row_info = new_top_ri; };
1,574✔
1333
    }
1334
}
11,279✔
1335

1336
void
1337
text_time_translator::data_reloaded(textview_curses* tc)
11,836✔
1338
{
1339
    if (tc->get_inner_height() == 0) {
11,836✔
1340
        this->ttt_top_row_info = std::nullopt;
10,076✔
1341
        return;
10,076✔
1342
    }
1343
    if (this->ttt_top_row_info) {
1,760✔
1344
        this->row_for(this->ttt_top_row_info.value()) |
409✔
1345
            [tc](auto new_top) { tc->set_selection(new_top); };
382✔
1346
    }
1347
}
1348

1349
template class bookmark_vector<vis_line_t>;
1350

1351
bool
1352
empty_filter::matches(std::optional<line_source> ls,
×
1353
                      const shared_buffer_ref& line)
1354
{
1355
    return false;
×
1356
}
1357

1358
std::string
1359
empty_filter::to_command() const
×
1360
{
1361
    return "";
×
1362
}
1363

1364
bool
1365
pcre_filter::matches(std::optional<line_source> ls,
1,561✔
1366
                     const shared_buffer_ref& line)
1367
{
1368
    auto options = 0;
1,561✔
1369
    if (line.get_metadata().m_valid_utf) {
1,561✔
1370
        options |= PCRE2_NO_UTF_CHECK;
1,561✔
1371
    }
1372

1373
    return this->pf_pcre->find_in(line.to_string_fragment(), options)
3,122✔
1374
        .ignore_error()
3,122✔
1375
        .has_value();
3,122✔
1376
}
1377

1378
filter_stack::iterator
1379
filter_stack::find(size_t index)
772✔
1380
{
1381
    for (auto iter = this->fs_filters.begin(); iter != this->fs_filters.end();
856✔
1382
         ++iter)
84✔
1383
    {
1384
        if ((*iter)->get_index() == index) {
91✔
1385
            return iter;
7✔
1386
        }
1387
    }
1388

1389
    return this->fs_filters.end();
765✔
1390
}
1391

1392
std::optional<size_t>
1393
filter_stack::next_index()
94✔
1394
{
1395
    bool used[32];
1396

1397
    memset(used, 0, sizeof(used));
94✔
1398
    for (auto& iter : *this) {
98✔
1399
        if (iter->lf_deleted) {
4✔
1400
            continue;
×
1401
        }
1402

1403
        size_t index = iter->get_index();
4✔
1404

1405
        require(used[index] == false);
4✔
1406

1407
        used[index] = true;
4✔
1408
    }
1409
    for (size_t lpc = this->fs_reserved;
98✔
1410
         lpc < logfile_filter_state::MAX_FILTERS;
98✔
1411
         lpc++)
1412
    {
1413
        if (!used[lpc]) {
98✔
1414
            return lpc;
94✔
1415
        }
1416
    }
1417
    return std::nullopt;
×
1418
}
1419

1420
std::shared_ptr<text_filter>
1421
filter_stack::get_filter(const std::string& id)
40✔
1422
{
1423
    auto iter = this->fs_filters.begin();
40✔
1424
    std::shared_ptr<text_filter> retval;
40✔
1425

1426
    for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {}
46✔
1427
    if (iter != this->fs_filters.end()) {
40✔
1428
        retval = *iter;
4✔
1429
    }
1430

1431
    return retval;
80✔
1432
}
×
1433

1434
bool
1435
filter_stack::delete_filter(const std::string& id)
9✔
1436
{
1437
    auto iter = this->fs_filters.begin();
9✔
1438

1439
    for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {}
9✔
1440
    if (iter != this->fs_filters.end()) {
9✔
1441
        this->fs_filters.erase(iter);
9✔
1442
        this->fs_generation += 1;
9✔
1443
        return true;
9✔
1444
    }
1445

1446
    return false;
×
1447
}
1448

1449
void
1450
filter_stack::get_mask(uint32_t& filter_mask)
×
1451
{
1452
    filter_mask = 0;
×
1453
    for (auto& iter : *this) {
×
1454
        std::shared_ptr<text_filter> tf = iter;
×
1455

1456
        if (tf->lf_deleted) {
×
1457
            continue;
×
1458
        }
1459
        if (tf->is_enabled()) {
×
1460
            uint32_t bit = (1UL << tf->get_index());
×
1461

1462
            switch (tf->get_type()) {
×
1463
                case text_filter::EXCLUDE:
×
1464
                case text_filter::INCLUDE:
1465
                    filter_mask |= bit;
×
1466
                    break;
×
1467
                default:
×
1468
                    ensure(0);
×
1469
                    break;
1470
            }
1471
        }
1472
    }
1473
}
1474

1475
void
1476
filter_stack::get_enabled_mask(uint32_t& filter_in_mask,
1,957✔
1477
                               uint32_t& filter_out_mask)
1478
{
1479
    filter_in_mask = filter_out_mask = 0;
1,957✔
1480
    for (auto& iter : *this) {
2,217✔
1481
        std::shared_ptr<text_filter> tf = iter;
260✔
1482

1483
        if (tf->lf_deleted) {
260✔
1484
            continue;
6✔
1485
        }
1486
        if (tf->is_enabled()) {
254✔
1487
            uint32_t bit = (1UL << tf->get_index());
105✔
1488

1489
            switch (tf->get_type()) {
105✔
1490
                case text_filter::EXCLUDE:
56✔
1491
                    filter_out_mask |= bit;
56✔
1492
                    break;
56✔
1493
                case text_filter::INCLUDE:
49✔
1494
                    filter_in_mask |= bit;
49✔
1495
                    break;
49✔
1496
                default:
×
1497
                    ensure(0);
×
1498
                    break;
1499
            }
1500
        }
1501
    }
260✔
1502
}
1,957✔
1503

1504
void
1505
filter_stack::add_filter(const std::shared_ptr<text_filter>& filter)
98✔
1506
{
1507
    this->fs_filters.push_back(filter);
98✔
1508
    this->fs_generation += 1;
98✔
1509
}
98✔
1510

1511
void
1512
vis_location_history::loc_history_append(vis_line_t top)
25✔
1513
{
1514
    auto iter = this->vlh_history.begin();
25✔
1515
    iter += this->vlh_history.size() - this->lh_history_position;
25✔
1516
    this->vlh_history.erase_from(iter);
25✔
1517
    this->lh_history_position = 0;
25✔
1518
    this->vlh_history.push_back(top);
25✔
1519
}
25✔
1520

1521
std::optional<vis_line_t>
1522
vis_location_history::loc_history_back(vis_line_t current_top)
×
1523
{
1524
    if (this->lh_history_position == 0) {
×
1525
        vis_line_t history_top = this->current_position();
×
1526
        if (history_top != current_top) {
×
1527
            return history_top;
×
1528
        }
1529
    }
1530

1531
    if (this->lh_history_position + 1 >= this->vlh_history.size()) {
×
1532
        return std::nullopt;
×
1533
    }
1534

1535
    this->lh_history_position += 1;
×
1536

1537
    return this->current_position();
×
1538
}
1539

1540
std::optional<vis_line_t>
1541
vis_location_history::loc_history_forward(vis_line_t current_top)
×
1542
{
1543
    if (this->lh_history_position == 0) {
×
1544
        return std::nullopt;
×
1545
    }
1546

1547
    this->lh_history_position -= 1;
×
1548

1549
    return this->current_position();
×
1550
}
1551

1552
void
1553
text_sub_source::update_filter_hash_state(hasher& h) const
16✔
1554
{
1555
    h.update(this->tss_filters.fs_generation);
16✔
1556

1557
    const auto* ttt = dynamic_cast<const text_time_translator*>(this);
16✔
1558
    if (ttt != nullptr) {
16✔
1559
        auto min_time = ttt->get_min_row_time();
16✔
1560
        if (min_time) {
16✔
1561
            h.update(min_time->tv_sec);
×
1562
            h.update(min_time->tv_usec);
×
1563
        } else {
1564
            h.update(0);
16✔
1565
            h.update(0);
16✔
1566
        }
1567
        auto max_time = ttt->get_max_row_time();
16✔
1568
        if (max_time) {
16✔
1569
            h.update(max_time->tv_sec);
×
1570
            h.update(max_time->tv_usec);
×
1571
        } else {
1572
            h.update(0);
16✔
1573
            h.update(0);
16✔
1574
        }
1575
    }
1576
}
16✔
1577

1578
void
1579
text_sub_source::toggle_apply_filters()
1✔
1580
{
1581
    this->tss_apply_filters = !this->tss_apply_filters;
1✔
1582
    this->text_filters_changed();
1✔
1583
}
1✔
1584

1585
void
1586
text_sub_source::text_crumbs_for_line(int line,
42✔
1587
                                      std::vector<breadcrumb::crumb>& crumbs)
1588
{
1589
}
42✔
1590

1591
logfile_filter_state::logfile_filter_state(std::shared_ptr<logfile> lf)
1,139✔
1592
    : tfs_logfile(std::move(lf))
1,139✔
1593
{
1594
    memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
1,139✔
1595
    memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
1,139✔
1596
    memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
1,139✔
1597
    memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
1,139✔
1598
    memset(this->tfs_last_message_matched,
1,139✔
1599
           0,
1600
           sizeof(this->tfs_last_message_matched));
1601
    memset(this->tfs_last_lines_for_message,
1,139✔
1602
           0,
1603
           sizeof(this->tfs_last_lines_for_message));
1604
    this->tfs_mask.reserve(64 * 1024);
1,139✔
1605
}
1,139✔
1606

1607
void
1608
logfile_filter_state::clear()
519✔
1609
{
1610
    this->tfs_logfile = nullptr;
519✔
1611
    this->clear_for_rebuild();
519✔
1612
}
519✔
1613

1614
void
1615
logfile_filter_state::clear_for_rebuild()
524✔
1616
{
1617
    memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
524✔
1618
    memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
524✔
1619
    memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
524✔
1620
    memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
524✔
1621
    memset(this->tfs_last_message_matched,
524✔
1622
           0,
1623
           sizeof(this->tfs_last_message_matched));
1624
    memset(this->tfs_last_lines_for_message,
524✔
1625
           0,
1626
           sizeof(this->tfs_last_lines_for_message));
1627
    this->tfs_mask.clear();
524✔
1628
    this->tfs_index.clear();
524✔
1629
}
524✔
1630

1631
void
1632
logfile_filter_state::clear_filter_state(size_t index)
3,882✔
1633
{
1634
    this->tfs_filter_count[index] = 0;
3,882✔
1635
    this->tfs_filter_hits[index] = 0;
3,882✔
1636
    this->tfs_message_matched[index] = false;
3,882✔
1637
    this->tfs_lines_for_message[index] = 0;
3,882✔
1638
    this->tfs_last_message_matched[index] = false;
3,882✔
1639
    this->tfs_last_lines_for_message[index] = 0;
3,882✔
1640
}
3,882✔
1641

1642
void
1643
logfile_filter_state::clear_deleted_filter_state(uint32_t used_mask)
122✔
1644
{
1645
    for (int lpc = 0; lpc < MAX_FILTERS; lpc++) {
4,026✔
1646
        if (!(used_mask & (1L << lpc))) {
3,904✔
1647
            this->clear_filter_state(lpc);
3,841✔
1648
        }
1649
    }
1650
    for (size_t lpc = 0; lpc < this->tfs_mask.size(); lpc++) {
1,572✔
1651
        this->tfs_mask[lpc] &= used_mask;
1,450✔
1652
    }
1653
}
122✔
1654

1655
void
1656
logfile_filter_state::resize(size_t newsize)
19,327✔
1657
{
1658
    size_t old_mask_size = this->tfs_mask.size();
19,327✔
1659

1660
    this->tfs_mask.resize(newsize);
19,327✔
1661
    if (newsize > old_mask_size) {
19,327✔
1662
        memset(&this->tfs_mask[old_mask_size],
18,353✔
1663
               0,
1664
               sizeof(uint32_t) * (newsize - old_mask_size));
18,353✔
1665
    }
1666
}
19,327✔
1667

1668
void
1669
logfile_filter_state::reserve(size_t expected)
2,350✔
1670
{
1671
    this->tfs_mask.reserve(expected);
2,350✔
1672
}
2,350✔
1673

1674
std::optional<size_t>
1675
logfile_filter_state::content_line_to_vis_line(uint32_t line)
×
1676
{
1677
    if (this->tfs_index.empty()) {
×
1678
        return std::nullopt;
×
1679
    }
1680

1681
    auto iter = std::lower_bound(
×
1682
        this->tfs_index.begin(), this->tfs_index.end(), line);
1683

1684
    if (iter == this->tfs_index.end() || *iter != line) {
×
1685
        return std::nullopt;
×
1686
    }
1687

1688
    return std::make_optional(std::distance(this->tfs_index.begin(), iter));
×
1689
}
1690

1691
std::string
1692
text_anchors::to_anchor_string(const std::string& raw)
60✔
1693
{
1694
    static const auto ANCHOR_RE = lnav::pcre2pp::code::from_const(R"([^\w]+)");
60✔
1695

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