summaryrefslogtreecommitdiffstats
path: root/src/base/attr_line.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/base/attr_line.cc')
-rw-r--r--src/base/attr_line.cc537
1 files changed, 537 insertions, 0 deletions
diff --git a/src/base/attr_line.cc b/src/base/attr_line.cc
new file mode 100644
index 0000000..db9c0f1
--- /dev/null
+++ b/src/base/attr_line.cc
@@ -0,0 +1,537 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "attr_line.hh"
+
+#include <stdarg.h>
+
+#include "ansi_scrubber.hh"
+#include "auto_mem.hh"
+#include "config.h"
+#include "lnav_log.hh"
+#include "pcrepp/pcre2pp.hh"
+
+attr_line_t&
+attr_line_t::with_ansi_string(const char* str, ...)
+{
+ auto_mem<char> formatted_str;
+ va_list args;
+
+ va_start(args, str);
+ auto ret = vasprintf(formatted_str.out(), str, args);
+ va_end(args);
+
+ if (ret >= 0 && formatted_str != nullptr) {
+ this->al_string = formatted_str;
+ scrub_ansi_string(this->al_string, &this->al_attrs);
+ }
+
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::with_ansi_string(const std::string& str)
+{
+ this->al_string = str;
+ scrub_ansi_string(this->al_string, &this->al_attrs);
+
+ return *this;
+}
+
+namespace text_stream {
+struct word {
+ string_fragment w_word;
+ string_fragment w_remaining;
+};
+
+struct space {
+ string_fragment s_value;
+ string_fragment s_remaining;
+};
+
+struct corrupt {
+ string_fragment c_value;
+ string_fragment c_remaining;
+};
+
+struct eof {
+ string_fragment e_remaining;
+};
+
+using chunk = mapbox::util::variant<word, space, corrupt, eof>;
+
+chunk
+consume(const string_fragment text)
+{
+ static const auto WORD_RE
+ = lnav::pcre2pp::code::from_const(R"((*UTF)^[^\p{Z}\p{So}\p{C}]+)");
+ static const auto SPACE_RE
+ = lnav::pcre2pp::code::from_const(R"((*UTF)^\s)");
+
+ if (text.empty()) {
+ return eof{text};
+ }
+
+ auto word_find_res
+ = WORD_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error();
+ if (word_find_res) {
+ auto split_res = text.split_n(word_find_res->f_all.length()).value();
+
+ return word{split_res.first, split_res.second};
+ }
+
+ if (isspace(text.front())) {
+ auto split_res = text.split_n(1).value();
+
+ return space{split_res.first, split_res.second};
+ }
+
+ auto space_find_res
+ = SPACE_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error();
+ if (space_find_res) {
+ auto split_res = text.split_n(space_find_res->f_all.length()).value();
+
+ return space{split_res.first, split_res.second};
+ }
+
+ auto csize_res = ww898::utf::utf8::char_size(
+ [&text]() { return std::make_pair(text.front(), text.length()); });
+
+ if (csize_res.isErr()) {
+ auto split_res = text.split_n(1);
+
+ return corrupt{split_res->first, split_res->second};
+ }
+
+ auto split_res = text.split_n(csize_res.unwrap());
+
+ return word{split_res->first, split_res->second};
+}
+
+} // namespace text_stream
+
+static void
+split_attrs(attr_line_t& al, const line_range& lr)
+{
+ if (lr.empty()) {
+ return;
+ }
+
+ string_attrs_t new_attrs;
+
+ for (auto& attr : al.al_attrs) {
+ if (!lr.intersects(attr.sa_range)) {
+ continue;
+ }
+
+ new_attrs.emplace_back(line_range{lr.lr_end, attr.sa_range.lr_end},
+ std::make_pair(attr.sa_type, attr.sa_value));
+ attr.sa_range.lr_end = lr.lr_start;
+ }
+ for (auto& new_attr : new_attrs) {
+ al.al_attrs.emplace_back(std::move(new_attr));
+ }
+}
+
+attr_line_t&
+attr_line_t::insert(size_t index,
+ const attr_line_t& al,
+ text_wrap_settings* tws)
+{
+ if (index < this->al_string.length()) {
+ shift_string_attrs(this->al_attrs, index, al.al_string.length());
+ }
+
+ this->al_string.insert(index, al.al_string);
+
+ for (const auto& sa : al.al_attrs) {
+ this->al_attrs.emplace_back(sa);
+
+ line_range& lr = this->al_attrs.back().sa_range;
+
+ lr.shift(0, index);
+ if (lr.lr_end == -1) {
+ lr.lr_end = index + al.al_string.length();
+ }
+ }
+
+ if (tws == nullptr) {
+ return *this;
+ }
+
+ auto starting_line_index = this->al_string.rfind('\n', index);
+ if (starting_line_index == std::string::npos) {
+ starting_line_index = 0;
+ } else {
+ starting_line_index += 1;
+ }
+
+ const ssize_t usable_width = tws->tws_width - tws->tws_indent;
+
+ auto text_to_wrap = string_fragment::from_str_range(
+ this->al_string, starting_line_index, this->al_string.length());
+ string_fragment last_word;
+ ssize_t line_ch_count = 0;
+ auto needs_indent = false;
+
+ while (!text_to_wrap.empty()) {
+ if (needs_indent) {
+ this->insert(text_to_wrap.sf_begin,
+ tws->tws_indent + tws->tws_padding_indent,
+ ' ');
+ auto indent_lr = line_range{
+ text_to_wrap.sf_begin,
+ text_to_wrap.sf_begin + tws->tws_indent,
+ };
+ split_attrs(*this, indent_lr);
+ indent_lr.lr_end += tws->tws_padding_indent;
+ line_ch_count += tws->tws_padding_indent;
+ if (!indent_lr.empty()) {
+ this->al_attrs.emplace_back(indent_lr, SA_PREFORMATTED.value());
+ }
+ text_to_wrap = text_to_wrap.prepend(
+ this->al_string.data(),
+ tws->tws_indent + tws->tws_padding_indent);
+ needs_indent = false;
+ }
+ auto chunk = text_stream::consume(text_to_wrap);
+
+ text_to_wrap = chunk.match(
+ [&](text_stream::word word) {
+ auto ch_count
+ = word.w_word.utf8_length().unwrapOr(word.w_word.length());
+
+ if ((line_ch_count + ch_count) > usable_width
+ && find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->insert(word.w_word.sf_begin, 1, '\n');
+ this->insert(word.w_word.sf_begin + 1,
+ tws->tws_indent + tws->tws_padding_indent,
+ ' ');
+ auto indent_lr = line_range{
+ word.w_word.sf_begin + 1,
+ word.w_word.sf_begin + 1 + tws->tws_indent,
+ };
+ split_attrs(*this, indent_lr);
+ indent_lr.lr_end += tws->tws_padding_indent;
+ if (!indent_lr.empty()) {
+ this->al_attrs.emplace_back(indent_lr,
+ SA_PREFORMATTED.value());
+ }
+ line_ch_count = tws->tws_padding_indent + ch_count;
+ auto trailing_space_count = 0;
+ if (!last_word.empty()) {
+ trailing_space_count
+ = word.w_word.sf_begin - last_word.sf_begin;
+ this->erase(last_word.sf_begin, trailing_space_count);
+ }
+ return word.w_remaining
+ .erase_before(this->al_string.data(),
+ trailing_space_count)
+ .prepend(this->al_string.data(),
+ 1 + tws->tws_indent + tws->tws_padding_indent);
+ }
+ line_ch_count += ch_count;
+
+ return word.w_remaining;
+ },
+ [&](text_stream::space space) {
+ if (space.s_value == "\n") {
+ line_ch_count = 0;
+ needs_indent = true;
+ return space.s_remaining;
+ }
+
+ if (line_ch_count > 0) {
+ auto ch_count = space.s_value.utf8_length().unwrapOr(
+ space.s_value.length());
+
+ if ((line_ch_count + ch_count) > usable_width
+ && find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->erase(space.s_value.sf_begin,
+ space.s_value.length());
+ this->insert(space.s_value.sf_begin, "\n");
+ line_ch_count = 0;
+ needs_indent = true;
+
+ auto trailing_space_count = 0;
+ if (!last_word.empty()) {
+ trailing_space_count
+ = space.s_value.sf_begin - last_word.sf_begin;
+ this->erase(last_word.sf_end, trailing_space_count);
+ }
+
+ return space.s_remaining
+ .erase_before(
+ this->al_string.data(),
+ space.s_value.length() + trailing_space_count)
+ .prepend(this->al_string.data(), 1);
+ }
+ line_ch_count += ch_count;
+ } else if (find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->erase(space.s_value.sf_begin, space.s_value.length());
+ return space.s_remaining.erase_before(
+ this->al_string.data(), space.s_value.length());
+ }
+
+ return space.s_remaining;
+ },
+ [](text_stream::corrupt corrupt) { return corrupt.c_remaining; },
+ [](text_stream::eof eof) { return eof.e_remaining; });
+
+ if (chunk.is<text_stream::word>()) {
+ last_word = text_to_wrap;
+ }
+
+ ensure(this->al_string.data() == text_to_wrap.sf_string);
+ ensure(text_to_wrap.sf_begin <= text_to_wrap.sf_end);
+ }
+ return *this;
+}
+
+attr_line_t
+attr_line_t::subline(size_t start, size_t len) const
+{
+ if (len == std::string::npos) {
+ len = this->al_string.length() - start;
+ }
+
+ line_range lr{(int) start, (int) (start + len)};
+ attr_line_t retval;
+
+ retval.al_string = this->al_string.substr(start, len);
+ for (const auto& sa : this->al_attrs) {
+ if (!lr.intersects(sa.sa_range)) {
+ continue;
+ }
+
+ auto ilr = lr.intersection(sa.sa_range).shift(0, -lr.lr_start);
+ retval.al_attrs.emplace_back(ilr,
+ std::make_pair(sa.sa_type, sa.sa_value));
+ const auto& last_lr = retval.al_attrs.back().sa_range;
+
+ ensure(last_lr.lr_end <= (int) retval.al_string.length());
+ }
+
+ return retval;
+}
+
+void
+attr_line_t::split_lines(std::vector<attr_line_t>& lines) const
+{
+ size_t pos = 0, next_line;
+
+ while ((next_line = this->al_string.find('\n', pos)) != std::string::npos) {
+ lines.emplace_back(this->subline(pos, next_line - pos));
+ pos = next_line + 1;
+ }
+ lines.emplace_back(this->subline(pos));
+}
+
+attr_line_t&
+attr_line_t::right_justify(unsigned long width)
+{
+ long padding = width - this->length();
+ if (padding > 0) {
+ this->al_string.insert(0, padding, ' ');
+ for (auto& al_attr : this->al_attrs) {
+ if (al_attr.sa_range.lr_start > 0) {
+ al_attr.sa_range.lr_start += padding;
+ }
+ if (al_attr.sa_range.lr_end != -1) {
+ al_attr.sa_range.lr_end += padding;
+ }
+ }
+ }
+
+ return *this;
+}
+
+size_t
+attr_line_t::nearest_text(size_t x) const
+{
+ if (x > 0 && x >= (size_t) this->length()) {
+ if (this->empty()) {
+ x = 0;
+ } else {
+ x = this->length() - 1;
+ }
+ }
+
+ while (x > 0 && isspace(this->al_string[x])) {
+ x -= 1;
+ }
+
+ return x;
+}
+
+void
+attr_line_t::apply_hide()
+{
+ auto& sa = this->al_attrs;
+
+ for (auto& sattr : sa) {
+ if (sattr.sa_type == &SA_HIDDEN && sattr.sa_range.length() > 3) {
+ struct line_range& lr = sattr.sa_range;
+
+ std::for_each(sa.begin(), sa.end(), [&](string_attr& attr) {
+ if (attr.sa_type == &VC_STYLE && lr.contains(attr.sa_range)) {
+ attr.sa_type = &SA_REMOVED;
+ }
+ });
+
+ this->al_string.replace(lr.lr_start, lr.length(), "\xE2\x8B\xAE");
+ shift_string_attrs(sa, lr.lr_start + 1, -(lr.length() - 3));
+ sattr.sa_type = &VC_ROLE;
+ sattr.sa_value = role_t::VCR_HIDDEN;
+ lr.lr_end = lr.lr_start + 3;
+ }
+ }
+}
+
+attr_line_t&
+attr_line_t::rtrim()
+{
+ auto index = this->al_string.length();
+
+ for (; index > 0; index--) {
+ if (find_string_attr_containing(
+ this->al_attrs, &SA_PREFORMATTED, index - 1)
+ != this->al_attrs.end())
+ {
+ break;
+ }
+ if (!isspace(this->al_string[index - 1])) {
+ break;
+ }
+ }
+ if (index > 0 && index < this->al_string.length()) {
+ this->erase(index);
+ }
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::erase(size_t pos, size_t len)
+{
+ if (len == std::string::npos) {
+ len = this->al_string.size() - pos;
+ }
+ if (len == 0) {
+ return *this;
+ }
+
+ this->al_string.erase(pos, len);
+
+ shift_string_attrs(this->al_attrs, pos, -((int32_t) len));
+ auto new_end = std::remove_if(
+ this->al_attrs.begin(), this->al_attrs.end(), [](const auto& attr) {
+ return attr.sa_range.empty();
+ });
+ this->al_attrs.erase(new_end, this->al_attrs.end());
+
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::pad_to(ssize_t size)
+{
+ const auto curr_len = this->utf8_length_or_length();
+
+ if (curr_len < size) {
+ this->append((size - curr_len), ' ');
+ for (auto& attr : this->al_attrs) {
+ if (attr.sa_range.lr_start == 0 && attr.sa_range.lr_end == curr_len)
+ {
+ attr.sa_range.lr_end = this->al_string.length();
+ }
+ }
+ }
+
+ return *this;
+}
+
+line_range
+line_range::intersection(const line_range& other) const
+{
+ int actual_end;
+
+ if (this->lr_end == -1) {
+ actual_end = other.lr_end;
+ } else if (other.lr_end == -1) {
+ actual_end = this->lr_end;
+ } else {
+ actual_end = std::min(this->lr_end, other.lr_end);
+ }
+ return line_range{std::max(this->lr_start, other.lr_start), actual_end};
+}
+
+line_range&
+line_range::shift(int32_t start, int32_t amount)
+{
+ if (start == this->lr_start) {
+ if (amount > 0) {
+ this->lr_start += amount;
+ }
+ if (this->lr_end != -1) {
+ this->lr_end += amount;
+ if (this->lr_end < this->lr_start) {
+ this->lr_end = this->lr_start;
+ }
+ }
+ } else if (start < this->lr_start) {
+ this->lr_start = std::max(0, this->lr_start + amount);
+ if (this->lr_end != -1) {
+ this->lr_end = std::max(0, this->lr_end + amount);
+ }
+ } else if (this->lr_end != -1) {
+ if (start < this->lr_end) {
+ if (amount < 0 && amount < (start - this->lr_end)) {
+ this->lr_end = start;
+ } else {
+ this->lr_end = std::max(this->lr_start, this->lr_end + amount);
+ }
+ }
+ }
+
+ return *this;
+}