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

tstack / lnav / 25348852825-3024

04 May 2026 11:18PM UTC coverage: 69.963% (+0.7%) from 69.226%
25348852825-3024

push

github

tstack
[ui] horizontal scroll should work on columns

Related to #1685

7 of 141 new or added lines in 5 files covered. (4.96%)

7760 existing lines in 84 files now uncovered.

57014 of 81492 relevant lines covered (69.96%)

622491.44 hits per line

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

76.33
/src/base/intern_string.cc
1
/**
2
 * Copyright (c) 2014, 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
 * @file intern_string.cc
30
 */
31

32
#include <mutex>
33

34
#include "intern_string.hh"
35

36
#include <string.h>
37

38
#include "config.h"
39
#include "fmt/ostream.h"
40
#include "lnav_log.hh"
41
#include "pcrepp/pcre2pp.hh"
42
#include "unictype.h"
43
#include "uniwidth.h"
44
#include "ww898/cp_utf8.hpp"
45
#include "xxHash/xxhash.h"
46

47
const static int TABLE_SIZE = 4095;
48

49
struct intern_string::intern_table {
50
    ~intern_table()
1,338✔
51
    {
52
        for (auto is : this->it_table) {
5,480,448✔
53
            auto curr = is;
5,479,110✔
54

55
            while (curr != nullptr) {
9,514,574✔
56
                auto next = curr->is_next;
4,035,464✔
57

58
                delete curr;
4,035,464✔
59
                curr = next;
4,035,464✔
60
            }
61
        }
62
    }
1,338✔
63

64
    intern_string* it_table[TABLE_SIZE];
65
};
66

67
intern_table_lifetime
68
intern_string::get_table_lifetime()
28,373,897✔
69
{
70
    static intern_table_lifetime retval = std::make_shared<intern_table>();
28,373,897✔
71

72
    return retval;
28,373,897✔
73
}
74

75
unsigned long
76
hash_str(const char* str, size_t len)
31,136,925✔
77
{
78
    return XXH3_64bits(str, len);
31,136,925✔
79
}
80

81
const intern_string*
82
intern_string::lookup(const char* str, ssize_t len) noexcept
28,365,364✔
83
{
84
    unsigned long h;
85
    intern_string* curr;
86

87
    if (len == -1) {
28,365,364✔
88
        len = strlen(str);
207,931✔
89
    }
90
    h = hash_str(str, len) % TABLE_SIZE;
28,365,364✔
91

92
    {
93
        static std::mutex table_mutex;
94

95
        std::lock_guard<std::mutex> lk(table_mutex);
28,365,364✔
96
        auto tab = get_table_lifetime();
28,365,364✔
97

98
        curr = tab->it_table[h];
28,365,364✔
99
        while (curr != nullptr) {
33,275,870✔
100
            if (static_cast<ssize_t>(curr->is_str.size()) == len
29,240,406✔
101
                && strncmp(curr->is_str.c_str(), str, len) == 0)
29,240,406✔
102
            {
103
                return curr;
24,329,900✔
104
            }
105
            curr = curr->is_next;
4,910,506✔
106
        }
107

108
        curr = new intern_string(str, len);
4,035,464✔
109
        curr->is_next = tab->it_table[h];
4,035,464✔
110
        tab->it_table[h] = curr;
4,035,464✔
111

112
        return curr;
4,035,464✔
113
    }
28,365,364✔
114
}
115

116
const intern_string*
117
intern_string::lookup(const string_fragment& sf) noexcept
11,052,773✔
118
{
119
    return lookup(sf.data(), sf.length());
11,052,773✔
120
}
121

122
const intern_string*
123
intern_string::lookup(const std::string& str) noexcept
10,130,630✔
124
{
125
    return lookup(str.c_str(), str.size());
10,130,630✔
126
}
127

128
bool
129
intern_string::startswith(const char* prefix) const
×
130
{
131
    const char* curr = this->is_str.data();
×
132

133
    while (*prefix != '\0' && *prefix == *curr) {
×
134
        prefix += 1;
×
135
        curr += 1;
×
136
    }
137

138
    return *prefix == '\0';
×
139
}
140

141
string_fragment
142
string_fragment::trim(const char* tokens) const
66,120✔
143
{
144
    string_fragment retval = *this;
66,120✔
145

146
    while (retval.sf_begin < retval.sf_end) {
108,754✔
147
        bool found = false;
108,592✔
148

149
        for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
351,893✔
150
            if (retval.sf_string[retval.sf_begin] == tokens[lpc]) {
285,935✔
151
                found = true;
42,634✔
152
                break;
42,634✔
153
            }
154
        }
155
        if (!found) {
108,592✔
156
            break;
65,958✔
157
        }
158

159
        retval.sf_begin += 1;
42,634✔
160
    }
161
    while (retval.sf_begin < retval.sf_end) {
67,155✔
162
        bool found = false;
66,993✔
163

164
        for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
312,483✔
165
            if (retval.sf_string[retval.sf_end - 1] == tokens[lpc]) {
246,525✔
166
                found = true;
1,035✔
167
                break;
1,035✔
168
            }
169
        }
170
        if (!found) {
66,993✔
171
            break;
65,958✔
172
        }
173

174
        retval.sf_end -= 1;
1,035✔
175
    }
176

177
    return retval;
66,120✔
178
}
179

180
string_fragment
181
string_fragment::trim() const
46,540✔
182
{
183
    return this->trim(" \t\r\n");
46,540✔
184
}
185

186
string_fragment
187
string_fragment::rtrim(const char* tokens) const
2,133✔
188
{
189
    string_fragment retval = *this;
2,133✔
190

191
    while (retval.sf_begin < retval.sf_end) {
4,221✔
192
        bool found = false;
4,182✔
193

194
        for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
6,276✔
195
            if (retval.sf_string[retval.sf_end - 1] == tokens[lpc]) {
4,182✔
196
                found = true;
2,088✔
197
                break;
2,088✔
198
            }
199
        }
200
        if (!found) {
4,182✔
201
            break;
2,094✔
202
        }
203

204
        retval.sf_end -= 1;
2,088✔
205
    }
206

207
    return retval;
2,133✔
208
}
209

210
std::optional<int>
211
string_fragment::rfind(char ch) const
×
212
{
213
    if (this->empty()) {
×
214
        return std::nullopt;
×
215
    }
216

217
    for (auto index = this->sf_end - 1; index >= this->sf_begin; index--) {
×
218
        if (this->sf_string[index] == ch) {
×
219
            return index;
×
220
        }
221
    }
222

223
    return std::nullopt;
×
224
}
225

226
std::optional<string_fragment>
227
string_fragment::consume_n(int amount) const
523✔
228
{
229
    if (amount > this->length()) {
523✔
230
        return std::nullopt;
×
231
    }
232

233
    return string_fragment{
523✔
234
        this->sf_string,
523✔
235
        this->sf_begin + amount,
523✔
236
        this->sf_end,
523✔
237
    };
523✔
238
}
239

240
string_fragment::split_n_result
241
string_fragment::split_n(int amount) const
329,442✔
242
{
243
    amount = std::min(amount, this->length());
329,442✔
244

245
    return {
246
        string_fragment{
658,884✔
247
            this->sf_string,
329,442✔
248
            this->sf_begin,
329,442✔
249
            this->sf_begin + amount,
329,442✔
250
        },
251
        string_fragment{
329,442✔
252
            this->sf_string,
329,442✔
253
            this->sf_begin + amount,
329,442✔
254
            this->sf_end,
329,442✔
255
        },
256
    };
658,884✔
257
}
258

259
std::vector<string_fragment>
260
string_fragment::split_lines() const
472,444✔
261
{
262
    std::vector<string_fragment> retval;
472,444✔
263
    int start = this->sf_begin;
472,444✔
264

265
    for (auto index = start; index < this->sf_end; index++) {
73,666,464✔
266
        if (this->sf_string[index] == '\n') {
73,194,020✔
267
            retval.emplace_back(this->sf_string, start, index + 1);
27,523✔
268
            start = index + 1;
27,523✔
269
        }
270
    }
271
    if (retval.empty() || start < this->sf_end) {
472,444✔
272
        retval.emplace_back(this->sf_string, start, this->sf_end);
471,885✔
273
    }
274

275
    return retval;
944,888✔
276
}
×
277

278
Result<ssize_t, const char*>
279
string_fragment::utf8_length() const
×
280
{
281
    ssize_t retval = 0;
×
282

283
    for (ssize_t byte_index = this->sf_begin; byte_index < this->sf_end;) {
×
284
        auto ch_size = TRY(ww898::utf::utf8::char_size([this, byte_index]() {
×
285
            return std::make_pair(this->sf_string[byte_index],
286
                                  this->sf_end - byte_index);
287
        }));
288
        byte_index += ch_size;
×
289
        retval += 1;
×
290
    }
291

292
    return Ok(retval);
×
293
}
294

295
string_fragment::case_style
296
string_fragment::detect_text_case_style() const
73✔
297
{
298
    static const auto LOWER_RE
299
        = lnav::pcre2pp::code::from_const(R"(^[^A-Z]+$)");
73✔
300
    static const auto UPPER_RE
301
        = lnav::pcre2pp::code::from_const(R"(^[^a-z]+$)");
73✔
302
    static const auto CAMEL_RE
303
        = lnav::pcre2pp::code::from_const(R"(^(?:[A-Z][a-z0-9]+)+$)");
73✔
304

305
    if (LOWER_RE.find_in(*this).ignore_error().has_value()) {
73✔
306
        return case_style::lower;
41✔
307
    }
308
    if (UPPER_RE.find_in(*this).ignore_error().has_value()) {
32✔
309
        return case_style::upper;
3✔
310
    }
311
    if (CAMEL_RE.find_in(*this).ignore_error().has_value()) {
29✔
312
        return case_style::camel;
16✔
313
    }
314

315
    return case_style::mixed;
13✔
316
}
317

318
std::string
319
string_fragment::to_string_with_case_style(case_style style) const
242✔
320
{
321
    std::string retval;
242✔
322

323
    switch (style) {
242✔
324
        case case_style::lower: {
41✔
325
            for (auto ch : *this) {
344✔
326
                retval.append(1, std::tolower(ch));
303✔
327
            }
328
            break;
41✔
329
        }
330
        case case_style::upper: {
172✔
331
            for (auto ch : *this) {
969✔
332
                retval.append(1, std::toupper(ch));
797✔
333
            }
334
            break;
172✔
335
        }
336
        case case_style::camel: {
16✔
337
            retval = this->to_string();
16✔
338
            if (!this->empty()) {
16✔
339
                retval[0] = toupper(retval[0]);
16✔
340
            }
341
            break;
16✔
342
        }
343
        case case_style::mixed: {
13✔
344
            return this->to_string();
13✔
345
        }
346
    }
347

348
    return retval;
229✔
349
}
242✔
350

351
uint64_t
352
string_fragment::bloom_bits() const
17,249✔
353
{
354
    auto a = XXH3_64bits(this->data(), this->length());
17,249✔
355
    auto b = a >> 8;
17,249✔
356
    if ((b & 0x3f) == (a & 0x3f)) {
17,249✔
357
        b = b >> 8;
110✔
358
    }
359
    auto c = b >> 8;
17,249✔
360
    if ((c & 0x3f) == (a & 0x3f) || (c & 0x3f) == (b & 0x3f)) {
17,249✔
361
        c = c >> 8;
476✔
362
    }
363

364
    uint64_t retval = 0;
17,249✔
365
    retval |= 1ULL << (a % 56);
17,249✔
366
    retval |= 1ULL << (b % 56);
17,249✔
367
    retval |= 1ULL << (c % 56);
17,249✔
368

369
    return retval;
17,249✔
370
}
371

372
std::string
373
string_fragment::to_unquoted_string() const
1,154✔
374
{
375
    auto sub_sf = *this;
1,154✔
376

377
    if (sub_sf.startswith("r") || sub_sf.startswith("u")) {
1,154✔
378
        sub_sf = sub_sf.consume_n(1).value();
24✔
379
    }
380
    if (sub_sf.length() >= 2
1,154✔
381
        && ((sub_sf.startswith("\"") && sub_sf.endswith("\""))
2,010✔
382
            || (sub_sf.startswith("'") && sub_sf.endswith("'"))))
856✔
383
    {
384
        std::string retval;
314✔
385

386
        sub_sf.sf_begin += 1;
314✔
387
        sub_sf.sf_end -= 1;
314✔
388
        retval.reserve(this->length());
314✔
389

390
        auto in_escape = false;
314✔
391
        for (auto ch : sub_sf) {
2,256✔
392
            if (in_escape) {
1,942✔
393
                switch (ch) {
×
394
                    case 'n':
×
395
                        retval.push_back('\n');
×
396
                        break;
×
397
                    case 't':
×
398
                        retval.push_back('\t');
×
399
                        break;
×
400
                    case 'r':
×
401
                        retval.push_back('\r');
×
402
                        break;
×
403
                    default:
×
404
                        retval.push_back(ch);
×
405
                        break;
×
406
                }
407
                in_escape = false;
×
408
            } else if (ch == '\\') {
1,942✔
409
                in_escape = true;
×
410
            } else {
411
                retval.push_back(ch);
1,942✔
412
            }
413
        }
414

415
        return retval;
314✔
416
    }
314✔
417

418
    return this->to_string();
840✔
419
}
420

421
uint32_t
422
string_fragment::front_codepoint() const
4,965,110✔
423
{
424
    size_t index = 0;
4,965,110✔
425
    auto read_res = ww898::utf::utf8::read(
426
        [this, &index]() { return this->data()[index++]; });
9,930,232✔
427
    if (read_res.isErr()) {
4,965,110✔
428
        return this->data()[0];
×
429
    }
430
    return read_res.unwrap();
4,965,110✔
431
}
4,965,110✔
432

433
Result<ssize_t, const char*>
434
string_fragment::codepoint_to_byte_index(ssize_t cp_index) const
4,977,818✔
435
{
436
    ssize_t retval = 0;
4,977,818✔
437

438
    while (cp_index > 0) {
9,438,354✔
439
        if (retval >= this->length()) {
4,977,818✔
440
            return Err("index is beyond the end of the string");
517,281✔
441
        }
442
        auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() {
8,921,074✔
443
            return std::make_pair(this->data()[retval],
444
                                  this->length() - retval - 1);
445
        }));
446

447
        retval += ch_len;
4,460,536✔
448
        cp_index -= 1;
4,460,536✔
449
    }
450

451
    return Ok(retval);
4,460,536✔
452
}
453

454
string_fragment
455
string_fragment::sub_cell_range(int cell_start, int cell_end) const
3✔
456
{
457
    int byte_index = this->sf_begin;
3✔
458
    std::optional<int> byte_start;
3✔
459
    std::optional<int> byte_end;
3✔
460
    int cell_index = 0;
3✔
461

462
    while (byte_index < this->sf_end) {
9✔
463
        if (cell_start == cell_index) {
6✔
464
            byte_start = byte_index;
3✔
465
        }
466
        if (!byte_end && cell_index >= cell_end) {
6✔
467
            byte_end = byte_index;
×
468
            break;
×
469
        }
470
        auto read_res = ww898::utf::utf8::read(
471
            [this, &byte_index]() { return this->sf_string[byte_index++]; });
12✔
472
        if (read_res.isErr()) {
6✔
473
            byte_index += 1;
×
474
        } else {
475
            auto ch = read_res.unwrap();
6✔
476

477
            switch (ch) {
6✔
478
                case '\t':
×
479
                    do {
480
                        cell_index += 1;
×
481
                    } while (cell_index % 8);
×
482
                    break;
×
483
                default: {
6✔
484
                    auto wcw_res = uc_width(read_res.unwrap(), "UTF-8");
6✔
485
                    if (wcw_res < 0) {
6✔
486
                        wcw_res = 1;
×
487
                    }
488
                    cell_index += wcw_res;
6✔
489
                    break;
6✔
490
                }
491
            }
492
        }
493
    }
6✔
494
    if (cell_start == cell_index) {
3✔
495
        byte_start = byte_index;
×
496
    }
497
    if (!byte_end) {
3✔
498
        byte_end = byte_index;
3✔
499
    }
500

501
    if (byte_start && byte_end) {
3✔
502
        return this->sub_range(byte_start.value(), byte_end.value());
3✔
503
    }
504

505
    return string_fragment{};
×
506
}
507

508
size_t
509
string_fragment::column_to_byte_index(const size_t col) const
324✔
510
{
511
    auto index = this->sf_begin;
324✔
512
    size_t curr_col = 0;
324✔
513

514
    while (curr_col < col && index < this->sf_end) {
651✔
515
        auto read_res = ww898::utf::utf8::read(
516
            [this, &index]() { return this->sf_string[index++]; });
1,559✔
517
        if (read_res.isErr()) {
327✔
518
            curr_col += 1;
×
519
        } else {
520
            auto ch = read_res.unwrap();
327✔
521

522
            switch (ch) {
327✔
523
                case '\t':
×
524
                    do {
525
                        curr_col += 1;
×
526
                    } while (curr_col % 8);
×
527
                    break;
×
528
                default: {
327✔
529
                    auto wcw_res = uc_width(read_res.unwrap(), "UTF-8");
327✔
530
                    if (wcw_res < 0) {
327✔
531
                        wcw_res = 1;
×
532
                    }
533

534
                    curr_col += wcw_res;
327✔
535
                    break;
327✔
536
                }
537
            }
538
        }
539
    }
327✔
540

541
    return index - this->sf_begin;
324✔
542
}
543

544
size_t
545
string_fragment::byte_to_column_index(const size_t byte_index) const
×
546
{
547
    auto index = this->sf_begin;
×
548
    size_t curr_col = 0;
×
549

550
    while (index < this->sf_end && index < (ssize_t) byte_index) {
×
551
        auto read_res = ww898::utf::utf8::read(
552
            [this, &index]() { return this->sf_string[index++]; });
×
553
        if (read_res.isErr()) {
×
554
            curr_col += 1;
×
555
        } else {
556
            auto ch = read_res.unwrap();
×
557

558
            switch (ch) {
×
559
                case '\t':
×
560
                    do {
561
                        curr_col += 1;
×
562
                    } while (curr_col % 8);
×
563
                    break;
×
564
                default: {
×
565
                    auto wcw_res = uc_width(read_res.unwrap(), "UTF-8");
×
566
                    if (wcw_res < 0) {
×
567
                        wcw_res = 1;
×
568
                    }
569

570
                    curr_col += wcw_res;
×
571
                    break;
×
572
                }
573
            }
574
        }
575
    }
576

577
    return curr_col;
×
578
}
579

580
enum class word_char_class {
581
    space,
582
    word,
583
    symbol,
584
};
585

586
static word_char_class
587
classify_word_char(wchar_t wchar)
463✔
588
{
589
    if (uc_is_property_white_space(wchar)) {
463✔
590
        return word_char_class::space;
65✔
591
    }
592
    static constexpr uint32_t word_mask
593
        = UC_CATEGORY_MASK_L | UC_CATEGORY_MASK_N | UC_CATEGORY_MASK_Pc;
594
    if (uc_is_general_category_withtable(wchar, word_mask)) {
398✔
595
        return word_char_class::word;
361✔
596
    }
597
    return word_char_class::symbol;
37✔
598
}
599

600
static bool
601
is_word_start(word_char_class curr_class, word_char_class prev_class)
390✔
602
{
603
    if (curr_class == word_char_class::word
390✔
604
        && prev_class != word_char_class::word)
309✔
605
    {
606
        return true;
91✔
607
    }
608
    if (curr_class == word_char_class::symbol
299✔
609
        && prev_class == word_char_class::space)
32✔
610
    {
611
        return true;
10✔
612
    }
613
    return false;
289✔
614
}
615

616
std::optional<int>
617
string_fragment::next_word(const int start_col) const
22✔
618
{
619
    auto index = this->sf_begin;
22✔
620
    int curr_col = 0;
22✔
621
    auto prev_class = word_char_class::space;
22✔
622

623
    while (index < this->sf_end) {
147✔
624
        auto read_res = ww898::utf::utf8::read(
625
            [this, &index]() { return this->sf_string[index++]; });
290✔
626
        if (read_res.isErr()) {
138✔
627
            curr_col += 1;
×
UNCOV
628
            continue;
×
629
        }
630
        auto ch = read_res.unwrap();
138✔
631

632
        if (ch == '\t') {
138✔
633
            prev_class = word_char_class::space;
1✔
634
            do {
635
                curr_col += 1;
7✔
636
            } while (curr_col % 8);
7✔
637
            continue;
1✔
638
        }
639

640
        auto wcw_res = uc_width(ch, "UTF-8");
137✔
641
        if (wcw_res < 0) {
137✔
642
            wcw_res = 1;
×
643
        }
644

645
        auto curr_class = classify_word_char(ch);
137✔
646
        if (curr_col > start_col && is_word_start(curr_class, prev_class)) {
137✔
647
            return curr_col;
13✔
648
        }
649
        prev_class = curr_class;
124✔
650
        curr_col += wcw_res;
124✔
651
    }
138✔
652

653
    return std::nullopt;
9✔
654
}
655

656
std::optional<int>
657
string_fragment::prev_word(const int start_col) const
28✔
658
{
659
    auto index = this->sf_begin;
28✔
660
    int curr_col = 0;
28✔
661
    auto prev_class = word_char_class::space;
28✔
662
    std::optional<int> last_word_col;
28✔
663

664
    while (index < this->sf_end) {
161✔
665
        auto read_res = ww898::utf::utf8::read(
666
            [this, &index]() { return this->sf_string[index++]; });
310✔
667
        if (read_res.isErr()) {
148✔
668
            curr_col += 1;
×
UNCOV
669
            continue;
×
670
        }
671
        auto ch = read_res.unwrap();
148✔
672

673
        if (ch == '\t') {
148✔
674
            if (curr_col >= start_col) {
2✔
675
                return last_word_col;
×
676
            }
677
            prev_class = word_char_class::space;
2✔
678
            do {
679
                curr_col += 1;
14✔
680
            } while (curr_col % 8);
14✔
681
            continue;
2✔
682
        }
683

684
        if (curr_col >= start_col) {
146✔
685
            return last_word_col;
15✔
686
        }
687

688
        auto wcw_res = uc_width(ch, "UTF-8");
131✔
689
        if (wcw_res < 0) {
131✔
UNCOV
690
            wcw_res = 1;
×
691
        }
692

693
        auto curr_class = classify_word_char(ch);
131✔
694
        if (is_word_start(curr_class, prev_class)) {
131✔
695
            last_word_col = curr_col;
34✔
696
        }
697
        prev_class = curr_class;
131✔
698
        curr_col += wcw_res;
131✔
699
    }
148✔
700

701
    return last_word_col;
13✔
702
}
703

704
std::optional<int>
705
string_fragment::curr_word(const int start_col) const
44✔
706
{
707
    auto index = this->sf_begin;
44✔
708
    int curr_col = 0;
44✔
709
    auto prev_class = word_char_class::space;
44✔
710
    std::optional<int> last_word_col;
44✔
711

712
    while (index < this->sf_end) {
197✔
713
        auto read_res = ww898::utf::utf8::read(
714
            [this, &index]() { return this->sf_string[index++]; });
422✔
715
        if (read_res.isErr()) {
195✔
UNCOV
716
            curr_col += 1;
×
UNCOV
717
            continue;
×
718
        }
719
        auto ch = read_res.unwrap();
195✔
720

721
        if (ch == '\t') {
195✔
UNCOV
722
            if (curr_col >= start_col) {
×
UNCOV
723
                return std::nullopt;
×
724
            }
UNCOV
725
            prev_class = word_char_class::space;
×
726
            do {
UNCOV
727
                curr_col += 1;
×
UNCOV
728
            } while (curr_col % 8);
×
UNCOV
729
            continue;
×
730
        }
731

732
        auto wcw_res = uc_width(ch, "UTF-8");
195✔
733
        if (wcw_res < 0) {
195✔
UNCOV
734
            wcw_res = 1;
×
735
        }
736

737
        auto curr_class = classify_word_char(ch);
195✔
738

739
        if (start_col < curr_col + wcw_res) {
195✔
740
            if (curr_class == word_char_class::space) {
42✔
741
                return std::nullopt;
8✔
742
            }
743
            if (is_word_start(curr_class, prev_class)) {
34✔
744
                return curr_col;
17✔
745
            }
746
            return last_word_col;
17✔
747
        }
748

749
        if (is_word_start(curr_class, prev_class)) {
153✔
750
            last_word_col = curr_col;
37✔
751
        }
752
        prev_class = curr_class;
153✔
753
        curr_col += wcw_res;
153✔
754
    }
195✔
755

756
    return std::nullopt;
2✔
757
}
758

759
std::string
UNCOV
760
string_fragment::transform_codepoints(
×
761
    const std::function<uint32_t(uint32_t)>& xform) const
762
{
UNCOV
763
    std::string out;
×
UNCOV
764
    out.reserve(this->length());
×
765

UNCOV
766
    auto index = this->sf_begin;
×
UNCOV
767
    while (index < this->sf_end) {
×
UNCOV
768
        auto byte_before = index;
×
769
        auto read_res = ww898::utf::utf8::read(
UNCOV
770
            [this, &index]() { return this->sf_string[index++]; });
×
UNCOV
771
        if (read_res.isErr()) {
×
UNCOV
772
            for (auto j = byte_before; j < index; ++j) {
×
UNCOV
773
                out.push_back(this->sf_string[j]);
×
774
            }
UNCOV
775
            continue;
×
776
        }
UNCOV
777
        auto cp = read_res.unwrap();
×
UNCOV
778
        auto new_cp = xform(cp);
×
UNCOV
779
        ww898::utf::utf8::write(new_cp,
×
UNCOV
780
                                [&out](const char b) { out.push_back(b); });
×
781
    }
UNCOV
782
    return out;
×
UNCOV
783
}
×
784

785
size_t
786
string_fragment::column_width() const
349,084✔
787
{
788
    auto index = this->sf_begin;
349,084✔
789
    size_t retval = 0;
349,084✔
790

791
    while (index < this->sf_end) {
2,685,331✔
792
        auto read_res = ww898::utf::utf8::read(
793
            [this, &index]() { return this->sf_string[index++]; });
4,735,038✔
794
        if (read_res.isErr()) {
2,336,247✔
795
            retval += 1;
6✔
796
        } else {
797
            auto ch = read_res.unwrap();
2,336,241✔
798

799
            switch (ch) {
2,336,241✔
800
                case '\t':
34,657✔
801
                    do {
802
                        retval += 1;
34,657✔
803
                    } while (retval % 8);
34,657✔
804
                    break;
6,196✔
805
                default: {
2,330,045✔
806
                    auto wcw_res = uc_width(read_res.unwrap(), "UTF-8");
2,330,045✔
807
                    if (wcw_res < 0) {
2,330,045✔
808
                        wcw_res = 1;
3,280✔
809
                    }
810
                    retval += wcw_res;
2,330,045✔
811
                    break;
2,330,045✔
812
                }
813
            }
814
        }
815
    }
2,336,247✔
816

817
    return retval;
349,084✔
818
}
819

820
std::optional<uint32_t>
821
string_fragment::cursor_impl::lookahead() const
121,493✔
822
{
823
    if (this->ci_next_index >= this->ci_end) {
121,493✔
824
        return std::nullopt;
1,360✔
825
    }
826

827
    int32_t index = this->ci_next_index;
120,133✔
828
    auto read_res = ww898::utf::utf8::read(
829
        [this, &index]() { return this->ci_string[index++]; });
240,268✔
830
    if (read_res.isErr()) {
120,133✔
UNCOV
831
        return this->ci_string[this->ci_next_index];
×
832
    }
833
    return read_res.unwrap();
120,133✔
834
}
120,133✔
835

836
std::optional<uint32_t>
837
string_fragment::cursor_impl::next()
121,496✔
838
{
839
    this->ci_lookbehind = this->ci_next_lookbehind;
121,496✔
840
    if (this->ci_next_index >= this->ci_end) {
121,496✔
841
        return std::nullopt;
1,361✔
842
    }
843

844
    int32_t index = this->ci_next_index;
120,135✔
845
    auto read_res = ww898::utf::utf8::read(
846
        [this, &index]() { return this->ci_string[index++]; });
240,272✔
847
    uint32_t retval;
848
    if (read_res.isErr()) {
120,135✔
UNCOV
849
        retval = this->ci_string[this->ci_next_index];
×
UNCOV
850
        this->ci_next_index += 1;
×
851
    } else {
852
        retval = read_res.unwrap();
120,135✔
853
        this->ci_next_index = index;
120,135✔
854
    }
855
    this->ci_next_lookbehind = retval;
120,135✔
856
    return retval;
120,135✔
857
}
120,135✔
858

859
struct single_producer : string_fragment_producer {
860
    explicit single_producer(const string_fragment& sf) : sp_frag(sf) {}
2,490✔
861

862
    next_result next() override
2,124✔
863
    {
864
        auto retval = std::exchange(this->sp_frag, std::nullopt);
2,124✔
865
        if (retval) {
2,124✔
866
            return retval.value();
1,062✔
867
        }
868

869
        return eof{};
1,062✔
870
    }
871

872
    std::optional<string_fragment> sp_frag;
873
};
874

875
std::unique_ptr<string_fragment_producer>
876
string_fragment_producer::from(string_fragment sf)
2,490✔
877
{
878
    return std::make_unique<single_producer>(sf);
2,490✔
879
}
880

881
std::string
882
string_fragment_producer::to_string()
20,049✔
883
{
884
    auto retval = std::string{};
20,049✔
885

886
    retval.reserve(this->estimated_size());
20,049✔
887
    auto for_res = this->for_each(
UNCOV
888
        [&retval](string_fragment sf) -> Result<void, std::string> {
×
889
            retval.append(sf.data(), sf.length());
26,092✔
890
            return Ok();
26,092✔
891
        });
20,049✔
892

893
    return retval;
40,098✔
894
}
20,049✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc