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

tstack / lnav / 19505555563-2683

19 Nov 2025 02:52PM UTC coverage: 68.872% (-0.005%) from 68.877%
19505555563-2683

push

github

tstack
[textview] fix clicking a wrapped link

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

3 existing lines in 1 file now uncovered.

51073 of 74156 relevant lines covered (68.87%)

431959.48 hits per line

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

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

30
#include <algorithm>
31
#include <string>
32
#include <vector>
33

34
#include "textview_curses.hh"
35

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

50
constexpr auto REVERSE_SEARCH_OFFSET = 2000_vl;
51

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

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

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

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

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

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

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

96
    return retval;
1,350✔
97
}
98

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

316
    listview_curses::invoke_scroll();
42,211✔
317
}
42,211✔
318

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

475
        ++row;
14,630✔
476
    }
477
}
14,528✔
478

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

856
    return true;
×
857
}
858

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

866
    auto format_attr_opt = get_string_attr(al.al_attrs, SA_FORMAT);
13,794✔
867
    if (format_attr_opt.has_value()) {
13,794✔
868
        format_name = format_attr_opt.value().get();
4,111✔
869
    }
870

871
    auto source_format = this->tc_sub_source->get_text_format();
13,794✔
872
    if (source_format == text_format_t::TF_BINARY) {
13,794✔
873
        return;
1,430✔
874
    }
875
    for (const auto& tc_highlight : this->tc_highlights) {
137,485✔
876
        bool internal_hl
125,121✔
877
            = tc_highlight.first.first == highlight_source_t::INTERNAL
125,121✔
878
            || tc_highlight.first.first == highlight_source_t::THEME;
125,121✔
879

880
        if (!tc_highlight.second.applies_to_format(source_format)) {
125,121✔
881
            continue;
44,253✔
882
        }
883

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

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

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

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

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

913
    for (const auto& attr : sa) {
116,251✔
914
        require_ge(attr.sa_range.lr_start, 0);
101,555✔
915
    }
916

917
    line_range body, orig_line;
14,696✔
918

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1191
        this->tc_source_search_child->start();
6✔
1192
    }
6✔
1193
}
159✔
1194

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

1203
    return true;
×
1204
}
1205

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

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

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

1249
    return std::nullopt;
×
1250
}
1251

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

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

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

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

1278
    if (ttt != nullptr) {
18,067✔
1279
        ttt->ttt_scroll_invoked(tc);
11,248✔
1280
    }
1281
}
18,067✔
1282

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

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

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

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

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

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

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

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

1342
template class bookmark_vector<vis_line_t>;
1343

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

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

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

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

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

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

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

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

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

1397
    return retval;
72✔
1398
}
×
1399

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

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

1413
    return false;
×
1414
}
1415

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

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

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

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

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

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

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

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

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

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

1502
    this->lh_history_position += 1;
×
1503

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

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

1514
    this->lh_history_position -= 1;
×
1515

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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