|
70 | 70 | #define ANSI_1_CTRL(ctrl1) "\033["##ctrl1 ANSI_ESC_END
|
71 | 71 | #define ANSI_2_CTRL(ctrl1, ctrl2) "\033["##ctrl1 ";"##ctrl2 ANSI_ESC_END
|
72 | 72 |
|
| 73 | +#define ANSI_ESC_START_LEN 2 |
| 74 | + |
73 | 75 | #include "llvm/ADT/ArrayRef.h"
|
74 | 76 | #include "llvm/ADT/STLExtras.h"
|
75 | 77 | #include "llvm/ADT/StringRef.h"
|
| 78 | +#include "llvm/Support/Locale.h" |
76 | 79 |
|
77 | 80 | #include <string>
|
78 | 81 |
|
@@ -172,28 +175,89 @@ inline std::string FormatAnsiTerminalCodes(llvm::StringRef format,
|
172 | 175 | return fmt;
|
173 | 176 | }
|
174 | 177 |
|
| 178 | +inline std::tuple<llvm::StringRef, llvm::StringRef, llvm::StringRef> |
| 179 | +FindNextAnsiSequence(llvm::StringRef str) { |
| 180 | + llvm::StringRef left; |
| 181 | + llvm::StringRef right = str; |
| 182 | + |
| 183 | + while (!right.empty()) { |
| 184 | + const size_t start = right.find(ANSI_ESC_START); |
| 185 | + |
| 186 | + // ANSI_ESC_START not found. |
| 187 | + if (start == llvm::StringRef::npos) |
| 188 | + return {str, {}, {}}; |
| 189 | + |
| 190 | + // Split the string around the current ANSI_ESC_START. |
| 191 | + left = str.take_front(left.size() + start); |
| 192 | + llvm::StringRef escape = right.substr(start); |
| 193 | + right = right.substr(start + ANSI_ESC_START_LEN + 1); |
| 194 | + |
| 195 | + const size_t end = right.find_first_not_of("0123456789;"); |
| 196 | + |
| 197 | + // ANSI_ESC_END found. |
| 198 | + if (end < right.size() && (right[end] == 'm' || right[end] == 'G')) |
| 199 | + return {left, escape.take_front(ANSI_ESC_START_LEN + 1 + end + 1), |
| 200 | + right.substr(end + 1)}; |
| 201 | + |
| 202 | + // Maintain the invariant that str == left + right at the start of the loop. |
| 203 | + left = str.take_front(left.size() + ANSI_ESC_START_LEN + 1); |
| 204 | + } |
| 205 | + |
| 206 | + return {str, {}, {}}; |
| 207 | +} |
| 208 | + |
175 | 209 | inline std::string StripAnsiTerminalCodes(llvm::StringRef str) {
|
176 | 210 | std::string stripped;
|
177 | 211 | while (!str.empty()) {
|
178 |
| - llvm::StringRef left, right; |
179 |
| - |
180 |
| - std::tie(left, right) = str.split(ANSI_ESC_START); |
| 212 | + auto [left, escape, right] = FindNextAnsiSequence(str); |
181 | 213 | stripped += left;
|
| 214 | + str = right; |
| 215 | + } |
| 216 | + return stripped; |
| 217 | +} |
182 | 218 |
|
183 |
| - // ANSI_ESC_START not found. |
184 |
| - if (left == str && right.empty()) |
185 |
| - break; |
| 219 | +inline std::string TrimAndPad(llvm::StringRef str, size_t visible_length, |
| 220 | + char padding = ' ') { |
| 221 | + std::string result; |
| 222 | + result.reserve(visible_length); |
| 223 | + size_t result_visibile_length = 0; |
186 | 224 |
|
187 |
| - size_t end = right.find_first_not_of("0123456789;"); |
188 |
| - if (end < right.size() && (right[end] == 'm' || right[end] == 'G')) { |
189 |
| - str = right.substr(end + 1); |
190 |
| - } else { |
191 |
| - // ANSI_ESC_END not found. |
192 |
| - stripped += ANSI_ESC_START; |
193 |
| - str = right; |
| 225 | + // Trim the string to the given visible length. |
| 226 | + while (!str.empty()) { |
| 227 | + auto [left, escape, right] = FindNextAnsiSequence(str); |
| 228 | + str = right; |
| 229 | + |
| 230 | + // Compute the length of the string without escape codes. If it fits, append |
| 231 | + // it together with the invisible escape code. |
| 232 | + size_t column_width = llvm::sys::locale::columnWidth(left); |
| 233 | + if (result_visibile_length + column_width <= visible_length) { |
| 234 | + result.append(left).append(escape); |
| 235 | + result_visibile_length += column_width; |
| 236 | + continue; |
| 237 | + } |
| 238 | + |
| 239 | + // The string might contain unicode which means it's not safe to truncate. |
| 240 | + // Repeatedly trim the string until it its valid unicode and fits. |
| 241 | + llvm::StringRef trimmed = left; |
| 242 | + while (!trimmed.empty()) { |
| 243 | + // This relies on columnWidth returning -2 for invalid/partial unicode |
| 244 | + // characters, which after conversion to size_t will be larger than the |
| 245 | + // visible width. |
| 246 | + column_width = llvm::sys::locale::columnWidth(trimmed); |
| 247 | + if (result_visibile_length + column_width <= visible_length) { |
| 248 | + result.append(trimmed); |
| 249 | + result_visibile_length += column_width; |
| 250 | + break; |
| 251 | + } |
| 252 | + trimmed = trimmed.drop_back(); |
194 | 253 | }
|
195 | 254 | }
|
196 |
| - return stripped; |
| 255 | + |
| 256 | + // Pad the string. |
| 257 | + if (result_visibile_length < visible_length) |
| 258 | + result.append(visible_length - result_visibile_length, padding); |
| 259 | + |
| 260 | + return result; |
197 | 261 | }
|
198 | 262 |
|
199 | 263 | } // namespace ansi
|
|
0 commit comments