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

tstack / lnav / 19505228778-2682

19 Nov 2025 02:42PM UTC coverage: 68.877% (-0.003%) from 68.88%
19505228778-2682

push

github

tstack
[textview] fix some word-wrap-related glitches

8 of 48 new or added lines in 3 files covered. (16.67%)

1 existing line in 1 file now uncovered.

51076 of 74155 relevant lines covered (68.88%)

431965.47 hits per line

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

59.19
/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,220✔
310
{
311
    this->tc_selected_text = std::nullopt;
42,220✔
312
    if (this->tc_sub_source != nullptr) {
42,220✔
313
        this->tc_sub_source->scroll_invoked(this);
18,832✔
314
    }
315

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

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

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

328
        if (ttt != nullptr) {
18,624✔
329
            ttt->data_reloaded(this);
11,814✔
330
        }
331
        listview_curses::reload_data();
18,624✔
332
    }
333
}
52,540✔
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: {
×
NEW
654
            auto same_line = (this->tc_press_line.is<main_content>()
×
NEW
655
                              && mouse_line.is<main_content>()
×
NEW
656
                              && this->tc_press_line.get<main_content>().mc_line
×
NEW
657
                                  == mouse_line.get<main_content>().mc_line);
×
658
            this->tc_text_selection_active = true;
×
659
            if (!this->vc_enabled) {
×
NEW
660
            } else if (same_line) {
×
NEW
661
                const auto& mc = mouse_line.get<main_content>();
×
NEW
662
                attr_line_t al;
×
663
                auto low_x
NEW
664
                    = std::min(this->tc_press_left,
×
NEW
665
                               (int) mc.mc_line_range.lr_start + me.me_x);
×
666
                auto high_x
NEW
667
                    = std::max(this->tc_press_left,
×
NEW
668
                               (int) mc.mc_line_range.lr_start + me.me_x);
×
669

NEW
670
                this->set_selection_without_context(mc.mc_line);
×
NEW
671
                if (this->tc_supports_marks
×
NEW
672
                    && me.me_button == mouse_button_t::BUTTON_LEFT)
×
673
                {
NEW
674
                    this->textview_value_for_row(mc.mc_line, al);
×
NEW
675
                    auto line_sf = string_fragment::from_str(al.get_string());
×
NEW
676
                    auto cursor_sf = line_sf.sub_cell_range(low_x, high_x);
×
NEW
677
                    if (this->lv_word_wrap) {
×
NEW
678
                    } else if (me.me_x <= 1) {
×
NEW
679
                        this->set_left(this->lv_left - 1);
×
NEW
680
                    } else if (me.me_x >= width - 1) {
×
NEW
681
                        this->set_left(this->lv_left + 1);
×
682
                    }
NEW
683
                    if (!cursor_sf.empty()) {
×
684
                        this->tc_selected_text = {
NEW
685
                            me.me_x,
×
NEW
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
                }
UNCOV
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(
×
774
                    this->lv_left + me.me_x, this->lv_left + me.me_x);
×
775
                auto link_iter = find_string_attr_containing(
×
776
                    al.get_attrs(), &VC_HYPERLINK, cursor_sf.sf_begin);
×
777
                if (link_iter != al.get_attrs().end()) {
×
778
                    auto href = link_iter->sa_value.get<std::string>();
×
779
                    auto* ta = dynamic_cast<text_anchors*>(this->tc_sub_source);
×
780

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

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

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

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

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

855
    return true;
×
856
}
857

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

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

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

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

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

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

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

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

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

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

916
    line_range body, orig_line;
14,696✔
917

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1190
        this->tc_source_search_child->start();
6✔
1191
    }
6✔
1192
}
162✔
1193

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

1202
    return true;
×
1203
}
1204

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

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

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

1248
    return std::nullopt;
×
1249
}
1250

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

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

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

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

1277
    if (ttt != nullptr) {
18,076✔
1278
        ttt->ttt_scroll_invoked(tc);
11,257✔
1279
    }
1280
}
18,076✔
1281

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

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

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

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

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

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

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

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

1341
template class bookmark_vector<vis_line_t>;
1342

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

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

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

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

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

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

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

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

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

1396
    return retval;
72✔
1397
}
×
1398

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

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

1412
    return false;
×
1413
}
1414

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

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

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

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

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

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

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

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

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

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

1501
    this->lh_history_position += 1;
×
1502

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

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

1513
    this->lh_history_position -= 1;
×
1514

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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