From 72fc06528abbbe7ca5e0fe50f150079e40f33a82 Mon Sep 17 00:00:00 2001 From: Alexander Khosrowshahi Date: Mon, 16 Jun 2025 17:47:31 -0400 Subject: [PATCH 1/3] Add support for string wrapping in formatter --- binaryninjaapi.h | 1 + binaryninjacore.h | 1 + formatter/generic/genericformatter.cpp | 232 ++++++++++++++++++++++--- lineformatter.cpp | 2 + 4 files changed, 215 insertions(+), 21 deletions(-) diff --git a/binaryninjaapi.h b/binaryninjaapi.h index 8ca9ad33f..4958aa4fe 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -14274,6 +14274,7 @@ namespace BinaryNinja { size_t desiredLineLength; size_t minimumContentLength; size_t tabWidth; + size_t maximumAnnotationLength; std::string languageName; std::string commentStartString; std::string commentEndString; diff --git a/binaryninjacore.h b/binaryninjacore.h index cdfbfcee4..f38a17704 100644 --- a/binaryninjacore.h +++ b/binaryninjacore.h @@ -3669,6 +3669,7 @@ extern "C" size_t desiredLineLength; size_t minimumContentLength; size_t tabWidth; + size_t maximumAnnotationLength; char* languageName; char* commentStartString; char* commentEndString; diff --git a/formatter/generic/genericformatter.cpp b/formatter/generic/genericformatter.cpp index ab7002c2d..42c7d8810 100644 --- a/formatter/generic/genericformatter.cpp +++ b/formatter/generic/genericformatter.cpp @@ -16,6 +16,11 @@ enum ItemType ArgumentSeparator, Statement, StatementSeparator, + StringComponent, + StringSeparator, + StringWhitespace, + FormatSpecifier, + EscapeSequence, Group, Container, StartOfContainer, @@ -250,6 +255,161 @@ static vector CreateStatementItems(const vector& items) return result; } +static vector ParseStringToken( + const InstructionTextToken& unprocessedStringToken, + const size_t maxParsingLength) +{ + const auto& src = unprocessedStringToken.text; + const size_t tail = src.size(); + + // Max parsing length set to max annotation length + if (tail > maxParsingLength) + return { unprocessedStringToken }; + vector result; + size_t curStart = 0, curEnd = 0; + + auto ConstructToken = [&](size_t start, size_t end) { + InstructionTextToken token = unprocessedStringToken; + const string newTxt = string(src.substr(start, end - start)); + token.text = newTxt; + token.width = newTxt.size(); + result.emplace_back(token); + }; + + auto flushToken = [&](size_t start, size_t end) + { + if (start < end) + ConstructToken(start, end); + }; + + // We generally split along spaces while keeping words intact, but some cases have + // specific splitting behavior: + // + // - Any format specifier (starting with %) will be treated as an atom even if embedded + // within a word + // - Any escape sequence will also be treated as an atom + // - We split along punctuation like commas, colons, periods, and semicolons, grouping + // trailing punctuation together. + while (curEnd < tail) + { + char c = src[curEnd]; + + if (c == '%') + { + // Flush before format specifier + flushToken(curStart, curEnd); + + size_t start = curEnd; + curEnd++; + while (curEnd < tail && (isalnum(src[curEnd]) || src[curEnd]=='.' || src[curEnd]=='-')) + curEnd++; + ConstructToken(start, curEnd); + curStart = curEnd; + } + else if (c == '\\') + { + // Flush before escape sequence + flushToken(curStart, curEnd); + + size_t start = curEnd; + curEnd++; // consume '\' + if (curEnd < tail) + curEnd++; // consume escaped char + ConstructToken(start, curEnd); + curStart = curEnd; + } + else if (c == ',' || c == '.' || c == ':' || c == ';' || isspace(c)) + { + // Flush before punctuation + flushToken(curStart, curEnd); + + // Group together repeated punctuation + size_t start = curEnd; + while (curEnd < tail && src[curEnd] == c) + curEnd++; + ConstructToken(start, curEnd); + curStart = curEnd; + } + else + { + curEnd++; + } + } + + flushToken(curStart, curEnd); + return result; +} + +static vector CreateStringGroups(const vector& items) +{ + vector result, pending; + bool hasStrings = false; + for (auto& i : items) + { + if (i.type == StringSeparator && !i.tokens.empty()) + { + // We try to push separators onto a preceding word, otherwise treat as + // a singular atom + if (pending.empty()) + { + result.push_back(Item {Atom, {}, {i.tokens}, 0}); + } + else + { + for (auto& j : i.tokens) + pending.back().AddTokenToLastAtom(j); + result.push_back(Item {StringComponent, pending, {}, 0}); + } + pending.clear(); + hasStrings = true; + } + else if (i.type == StringWhitespace) + { + // Special case because we let whitespace trail even if over width + if (!pending.empty()) + { + result.push_back(Item {StringComponent, pending, {}, 0}); + pending.clear(); + } + result.push_back(Item {StringWhitespace, i.items, i.tokens, i.width}); + } + else if (i.type == FormatSpecifier || i.type == EscapeSequence) + { + // Flush previous tokens before special sequences like format specifiers or + // escape sequences + if (!pending.empty()) + { + result.push_back(Item {StringComponent, pending, {}, 0 }); + pending.clear(); + } + result.push_back(Item { Atom, i.items, i.tokens, i.width}); + } + + else if (i.type == StartOfContainer && pending.empty()) + { + result.push_back(i); + } + else if (i.type == EndOfContainer && hasStrings && !pending.empty()) + { + result.push_back(Item {StringComponent, pending, {}, 0}); + result.push_back(i); + } + else + { + pending.push_back(Item {i.type, CreateStringGroups(i.items), i.tokens, 0}); + } + } + + if (!pending.empty()) + { + if (hasStrings) + result.push_back(Item {StringComponent, pending, {}, 0}); + else + result.insert(result.end(), pending.begin(), pending.end()); + } + + return result; +} static vector CreateAssignmentOperatorGroups(const vector& items) { @@ -576,8 +736,7 @@ vector GenericLineFormatter::FormatLines( size_t tokenIndex = indentationTokens.size(); // First break the line down into nested container items. A container is anything between a pair of - // BraceTokens (except for strings, where the entire string, including the quotes, are treated as - // a single atom). + // BraceTokens vector items; stack> itemStack; for (; tokenIndex < currentLine.tokens.size(); tokenIndex++) @@ -588,29 +747,30 @@ vector GenericLineFormatter::FormatLines( switch (token.type) { case BraceToken: + // Beginning of string if (tokenIndex + 1 < currentLine.tokens.size() && currentLine.tokens[tokenIndex + 1].type == StringToken) { - // Treat string tokens surrounded by brace tokens as a unit (this is usually the quotes - // surrounding the string) - Item atom; - atom.type = Atom; - atom.tokens.push_back(token); - atom.tokens.push_back(currentLine.tokens[tokenIndex + 1]); - atom.width = 0; - tokenIndex++; - if (tokenIndex + 1 < currentLine.tokens.size() - && currentLine.tokens[tokenIndex + 1].type == BraceToken) - { - atom.tokens.push_back(currentLine.tokens[tokenIndex + 1]); - tokenIndex++; - } + // Create a ContainerContents item and place it onto the item stack. This will hold anything + // inside the container once the end of the container is found. + items.push_back(Item {Container, {}, {}, 0}); + itemStack.push(items); - items.push_back(atom); - break; + // Starting a new context + items.clear(); + items.push_back(Item {StartOfContainer, {}, {token}, 0}); } - - if (trimmedText == "(" || trimmedText == "[" || trimmedText == "{") + // End of string + else if (currentLine.tokens[tokenIndex].type == StringToken + && tokenIndex + 1 < currentLine.tokens.size() + && currentLine.tokens[tokenIndex + 1].type == BraceToken) + { + // Create a ContainerContents item and place it onto the item stack. This will hold anything + // inside the container once the end of the container is found. + items.push_back(Item {Container, {}, {}, 0}); + itemStack.push(items); + } + else if (trimmedText == "(" || trimmedText == "[" || trimmedText == "{") { // Create a ContainerContents item and place it onto the item stack. This will hold anything // inside the container once the end of the container is found. @@ -663,6 +823,25 @@ vector GenericLineFormatter::FormatLines( else items.push_back(Item {Operator, {}, {token}, 0}); break; + case StringToken: + { + vector stringTokens = ParseStringToken(token, settings.maximumAnnotationLength); + for (auto subToken : stringTokens) + { + string trimmedSubText = TrimString(subToken.text); + if (trimmedSubText.empty()) + items.push_back(Item {StringWhitespace, {}, {subToken}, 0}); + if (trimmedSubText[0] == '%') + items.push_back(Item {FormatSpecifier, {}, {subToken}, 0}); + else if (!trimmedSubText.empty() && trimmedSubText[0] == '\\') + items.push_back(Item {EscapeSequence, {}, {subToken}, 0}); + else if (trimmedSubText[0] == ',' || trimmedSubText[0] == '.' || trimmedSubText[0] == ':' || trimmedSubText[0] == ';') + items.push_back(Item {StringSeparator, {}, {subToken}, 0}); + else + items.push_back(Item {Atom, {}, {subToken}, 0}); + } + break; + } default: items.push_back(Item {Atom, {}, {token}, 0}); break; @@ -699,6 +878,10 @@ vector GenericLineFormatter::FormatLines( // the previous atom. items = RelocateStartAndEndOfContainerItems(items); + // Create internal groupings for displaying strings -- grouping items by punctuation, format specifiers, and + // escape sequences + items = CreateStringGroups(items); + // Now that items are done, compute widths for layout for (auto& j : items) j.CalculateWidth(); @@ -754,9 +937,16 @@ vector GenericLineFormatter::FormatLines( for (auto item = items.begin(); item != items.end();) { - if (currentWidth + item->width > desiredWidth) + if (item->type == StringComponent && currentWidth + item->width > desiredWidth) + { + // If a string is too wide to fit on the current line, create a newline + // without additional indentation + newLine(); + } + else if (currentWidth + item->width > desiredWidth && item->type != StringWhitespace) { // Current item is too wide to fit on the current line, will need to start a new line. + // Whitespace is allowed to be too wide; we push it on as the preceding word is wrapped. auto next = item; ++next; diff --git a/lineformatter.cpp b/lineformatter.cpp index 75b220288..16301316a 100644 --- a/lineformatter.cpp +++ b/lineformatter.cpp @@ -53,6 +53,7 @@ LineFormatterSettings LineFormatterSettings::FromAPIObject(const BNLineFormatter result.desiredLineLength = settings->desiredLineLength; result.minimumContentLength = settings->minimumContentLength; result.tabWidth = settings->tabWidth; + result.maximumAnnotationLength = settings->maximumAnnotationLength; result.languageName = settings->languageName; result.commentStartString = settings->commentStartString; result.commentEndString = settings->commentEndString; @@ -69,6 +70,7 @@ BNLineFormatterSettings LineFormatterSettings::ToAPIObject() const result.desiredLineLength = desiredLineLength; result.minimumContentLength = minimumContentLength; result.tabWidth = tabWidth; + result.maximumAnnotationLength = maximumAnnotationLength; result.languageName = (char*)languageName.c_str(); result.commentStartString = (char*)commentStartString.c_str(); result.commentEndString = (char*)commentEndString.c_str(); From 158b2af9b6b23db0edd1789f296600aa6ac39ba5 Mon Sep 17 00:00:00 2001 From: Alexander Khosrowshahi Date: Tue, 17 Jun 2025 13:15:10 -0400 Subject: [PATCH 2/3] Fix doubling spaces on wrapped strings --- binaryninjaapi.h | 1 + binaryninjacore.h | 1 + formatter/generic/genericformatter.cpp | 58 ++++++++++++++++++++++---- lineformatter.cpp | 2 + 4 files changed, 53 insertions(+), 9 deletions(-) diff --git a/binaryninjaapi.h b/binaryninjaapi.h index 4958aa4fe..8414adddc 100644 --- a/binaryninjaapi.h +++ b/binaryninjaapi.h @@ -14275,6 +14275,7 @@ namespace BinaryNinja { size_t minimumContentLength; size_t tabWidth; size_t maximumAnnotationLength; + size_t stringWrappingWidth; std::string languageName; std::string commentStartString; std::string commentEndString; diff --git a/binaryninjacore.h b/binaryninjacore.h index f38a17704..b2047eee9 100644 --- a/binaryninjacore.h +++ b/binaryninjacore.h @@ -3670,6 +3670,7 @@ extern "C" size_t minimumContentLength; size_t tabWidth; size_t maximumAnnotationLength; + size_t stringWrappingWidth; char* languageName; char* commentStartString; char* commentEndString; diff --git a/formatter/generic/genericformatter.cpp b/formatter/generic/genericformatter.cpp index 42c7d8810..7e25f1f48 100644 --- a/formatter/generic/genericformatter.cpp +++ b/formatter/generic/genericformatter.cpp @@ -203,6 +203,8 @@ struct ItemLayoutStackEntry size_t additionalContinuationIndentation; size_t desiredWidth; size_t desiredContinuationWidth; + size_t desiredStringWidth; + size_t desiredStringContinuationWidth; bool newLineOnReenteringScope; }; @@ -318,7 +320,18 @@ static vector ParseStringToken( ConstructToken(start, curEnd); curStart = curEnd; } - else if (c == ',' || c == '.' || c == ':' || c == ';' || isspace(c)) + else if (isspace(c)) + { + // Flush before whitespace + flushToken(curStart, curEnd); + + size_t start = curEnd; + while (curEnd < tail && isspace(src[curEnd])) + curEnd++; + ConstructToken(start, curEnd); + curStart = curEnd; + } + else if (c == ',' || c == '.' || c == ':' || c == ';') { // Flush before punctuation flushToken(curStart, curEnd); @@ -731,6 +744,24 @@ vector GenericLineFormatter::FormatLines( desiredContinuationWidth = remainingWidth; } + // Compute target string width for this line + size_t desiredStringWidth = settings.stringWrappingWidth; + if (indentation < settings.desiredLineLength) + { + size_t remainingStringWidth = desiredStringWidth - indentation; + if (remainingStringWidth > desiredStringWidth) + desiredStringWidth = remainingStringWidth; + } + + // Compute target width for continuation string wrapping lines + size_t desiredStringContinuationWidth = settings.stringWrappingWidth; + if (continuationIndentation < settings.desiredLineLength) + { + size_t remainingStringWidth = desiredStringContinuationWidth - continuationIndentation; + if (remainingStringWidth > desiredStringContinuationWidth) + desiredStringContinuationWidth = remainingStringWidth; + } + // Gather the indentation tokens at the beginning of the line vector indentationTokens = currentLine.GetAddressAndIndentationTokens(); size_t tokenIndex = indentationTokens.size(); @@ -831,7 +862,7 @@ vector GenericLineFormatter::FormatLines( string trimmedSubText = TrimString(subToken.text); if (trimmedSubText.empty()) items.push_back(Item {StringWhitespace, {}, {subToken}, 0}); - if (trimmedSubText[0] == '%') + else if (trimmedSubText[0] == '%') items.push_back(Item {FormatSpecifier, {}, {subToken}, 0}); else if (!trimmedSubText.empty() && trimmedSubText[0] == '\\') items.push_back(Item {EscapeSequence, {}, {subToken}, 0}); @@ -892,7 +923,7 @@ vector GenericLineFormatter::FormatLines( bool firstTokenOfLine = true; stack layoutStack; - layoutStack.push({items, additionalContinuationIndentation, desiredWidth, desiredContinuationWidth, false}); + layoutStack.push({items, additionalContinuationIndentation, desiredWidth, desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); auto newLine = [&]() { if (!firstTokenOfLine) @@ -917,6 +948,7 @@ vector GenericLineFormatter::FormatLines( outputLine.tokens.emplace_back(TextToken, string(additionalContinuationIndentation, ' ')); currentWidth = 0; desiredWidth = desiredContinuationWidth; + desiredStringWidth = desiredStringContinuationWidth; firstTokenOfLine = true; }; @@ -929,6 +961,8 @@ vector GenericLineFormatter::FormatLines( additionalContinuationIndentation = layoutStackEntry.additionalContinuationIndentation; desiredWidth = layoutStackEntry.desiredWidth; desiredContinuationWidth = layoutStackEntry.desiredContinuationWidth; + desiredStringWidth = layoutStackEntry.desiredStringWidth; + desiredStringContinuationWidth = layoutStackEntry.desiredStringContinuationWidth; // Check to see if the scope we are returning to needs a new line. This is used when an argument // spans multiple lines. The rest of the arguments are placed on separate lines from the long argument. @@ -937,13 +971,14 @@ vector GenericLineFormatter::FormatLines( for (auto item = items.begin(); item != items.end();) { - if (item->type == StringComponent && currentWidth + item->width > desiredWidth) + if (item->type == StringComponent && currentWidth + item->width > desiredStringWidth) { // If a string is too wide to fit on the current line, create a newline // without additional indentation newLine(); + continue; } - else if (currentWidth + item->width > desiredWidth && item->type != StringWhitespace) + if (currentWidth + item->width > desiredWidth && item->type != StringWhitespace) { // Current item is too wide to fit on the current line, will need to start a new line. // Whitespace is allowed to be too wide; we push it on as the preceding word is wrapped. @@ -962,7 +997,7 @@ vector GenericLineFormatter::FormatLines( if (next != items.end()) { layoutStack.push({vector(next, items.end()), additionalContinuationIndentation, - desiredWidth, desiredContinuationWidth, true}); + desiredWidth, desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, true}); } newLine(); @@ -973,8 +1008,13 @@ vector GenericLineFormatter::FormatLines( else desiredContinuationWidth -= settings.tabWidth; + if (desiredStringContinuationWidth < settings.minimumContentLength + settings.tabWidth) + desiredStringContinuationWidth = settings.minimumContentLength; + else + desiredStringContinuationWidth -= settings.tabWidth; + layoutStack.push({item->items, additionalContinuationIndentation, desiredWidth, - desiredContinuationWidth, false}); + desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); break; } @@ -984,10 +1024,10 @@ vector GenericLineFormatter::FormatLines( if (next != items.end()) { layoutStack.push({vector(next, items.end()), additionalContinuationIndentation, - desiredWidth, desiredContinuationWidth, false}); + desiredWidth, desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); } layoutStack.push({item->items, additionalContinuationIndentation, desiredWidth, - desiredContinuationWidth, false}); + desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); break; } diff --git a/lineformatter.cpp b/lineformatter.cpp index 16301316a..44eac0b3e 100644 --- a/lineformatter.cpp +++ b/lineformatter.cpp @@ -54,6 +54,7 @@ LineFormatterSettings LineFormatterSettings::FromAPIObject(const BNLineFormatter result.minimumContentLength = settings->minimumContentLength; result.tabWidth = settings->tabWidth; result.maximumAnnotationLength = settings->maximumAnnotationLength; + result.stringWrappingWidth = settings->stringWrappingWidth; result.languageName = settings->languageName; result.commentStartString = settings->commentStartString; result.commentEndString = settings->commentEndString; @@ -71,6 +72,7 @@ BNLineFormatterSettings LineFormatterSettings::ToAPIObject() const result.minimumContentLength = minimumContentLength; result.tabWidth = tabWidth; result.maximumAnnotationLength = maximumAnnotationLength; + result.stringWrappingWidth = stringWrappingWidth; result.languageName = (char*)languageName.c_str(); result.commentStartString = (char*)commentStartString.c_str(); result.commentEndString = (char*)commentEndString.c_str(); From 6d1ec48a626d918405e7d3fd21ca361227d9f925 Mon Sep 17 00:00:00 2001 From: Alexander Khosrowshahi Date: Tue, 24 Jun 2025 10:47:49 -0400 Subject: [PATCH 3/3] String formatting fixes and efficiency improvements --- formatter/generic/genericformatter.cpp | 703 ++++++++++++++++--------- 1 file changed, 460 insertions(+), 243 deletions(-) diff --git a/formatter/generic/genericformatter.cpp b/formatter/generic/genericformatter.cpp index 7e25f1f48..07c6d6671 100644 --- a/formatter/generic/genericformatter.cpp +++ b/formatter/generic/genericformatter.cpp @@ -159,7 +159,7 @@ struct Item string trimmedText = TrimLeadingWhitespace(token.text); token.width -= token.text.size() - trimmedText.size(); token.text = trimmedText; - output.push_back(token); + output.emplace_back(token); output.insert(output.end(), tokens.begin() + 1, tokens.end()); firstTokenOfLine = false; } @@ -176,13 +176,23 @@ struct Item void AddTokenToLastAtom(const InstructionTextToken& token) { if (!tokens.empty()) - tokens.push_back(token); + tokens.emplace_back(token); else if (items.empty()) - items.push_back(Item {Atom, {}, {token}, 0}); + items.emplace_back(Item {Atom, {}, {token}, 0}); else items.back().AddTokenToLastAtom(token); } + void AddTokenToLastStringComponent(const InstructionTextToken& token) + { + if (!tokens.empty()) + tokens.emplace_back(token); + else if (items.empty()) + items.emplace_back(Item {StringComponent, {}, {token}, 0}); + else + items.back().AddTokenToLastStringComponent(token); + } + void CalculateWidth() { width = 0; @@ -204,79 +214,125 @@ struct ItemLayoutStackEntry size_t desiredWidth; size_t desiredContinuationWidth; size_t desiredStringWidth; - size_t desiredStringContinuationWidth; bool newLineOnReenteringScope; }; - static vector CreateStatementItems(const vector& items) { - vector result, pending; - bool hasArgs = false; - for (auto& i : items) - { - if (i.type == StatementSeparator) - { - if (pending.empty()) - { - result.push_back(Item {Atom, {}, {i.tokens}, 0}); - } - else - { - for (auto& j : i.tokens) - pending.back().AddTokenToLastAtom(j); - result.push_back(Item {Statement, pending, {}, 0}); - } - pending.clear(); - hasArgs = true; - } - else if (i.type == StartOfContainer && pending.empty()) - { - result.push_back(i); - } - else if (i.type == EndOfContainer && hasArgs && !pending.empty()) - { - result.push_back(Item {Statement, pending, {}, 0}); - result.push_back(i); - pending.clear(); - } - else - { - pending.push_back(Item {i.type, CreateStatementItems(i.items), i.tokens, 0}); - } - } + vector result; + result.reserve(items.size()); + + vector pending; + pending.reserve(items.size()); + + bool hasArgs = false; + + auto flushStatement = [&]() { + if (!pending.empty()) { + result.emplace_back( + Item{ + Statement, + std::move(pending), + vector{}, + 0 + } + ); + pending.clear(); + } + }; - if (!pending.empty()) - { - if (hasArgs) - result.push_back(Item {Statement, pending, {}, 0}); - else - result.insert(result.end(), pending.begin(), pending.end()); - } + for (auto const& orig : items) { + // copy so we can move out of i.tokens / i.items safely + Item i = orig; + + if (i.type == StatementSeparator) { + hasArgs = true; + + if (pending.empty()) { + // first separator in this statement + result.emplace_back( + Item{ + Atom, + {}, + std::move(i.tokens), + 0 + } + ); + } else { + // append tokens to last atom in pending, then flush + for (auto& t : i.tokens) + pending.back().AddTokenToLastAtom(std::move(t)); + flushStatement(); + } + } + else if (i.type == StartOfContainer && pending.empty()) { + // emit container boundary directly if no pending args + result.emplace_back(std::move(i)); + } + else if (i.type == EndOfContainer && hasArgs && !pending.empty()) { + // flush any accumulated args as a Statement, then emit the container end + flushStatement(); + result.emplace_back(std::move(i)); + } + else { + // accumulate everything else for possible statement grouping + vector nested; + if (!i.items.empty()) + nested = CreateStatementItems(i.items); + + pending.emplace_back( + Item{ + i.type, + std::move(nested), + std::move(i.tokens), + 0 + } + ); + } + } - return result; + // final tail: either flush into a Statement or append raw + if (!pending.empty()) { + if (hasArgs) { + flushStatement(); + } else { + result.insert( + result.end(), + make_move_iterator(pending.begin()), + make_move_iterator(pending.end()) + ); + } + } + + return result; } static vector ParseStringToken( const InstructionTextToken& unprocessedStringToken, - const size_t maxParsingLength) + const size_t maxParsingLength +) { - const auto& src = unprocessedStringToken.text; - const size_t tail = src.size(); - - // Max parsing length set to max annotation length - if (tail > maxParsingLength) - return { unprocessedStringToken }; - vector result; - size_t curStart = 0, curEnd = 0; - - auto ConstructToken = [&](size_t start, size_t end) { - InstructionTextToken token = unprocessedStringToken; - const string newTxt = string(src.substr(start, end - start)); - token.text = newTxt; - token.width = newTxt.size(); - result.emplace_back(token); - }; + const string_view src = unprocessedStringToken.text; + const size_t tail = src.size(); + + // Max parsing length set to max annotation length + if (tail > maxParsingLength) + return {unprocessedStringToken}; + + vector result; + size_t curStart = 0, curEnd = 0; + + string scratch; + scratch.reserve(maxParsingLength); + + auto ConstructToken = [&](size_t start, size_t end) + { + scratch.assign(src.data() + start, end - start); + InstructionTextToken token = unprocessedStringToken; + token.text.swap(scratch); + token.width = token.text.size(); + result.emplace_back(std::move(token)); + }; auto flushToken = [&](size_t start, size_t end) { @@ -355,79 +411,130 @@ static vector ParseStringToken( static vector CreateStringGroups(const vector& items) { - vector result, pending; + // We handle strings mostly the same as other types except the introduction + // of the StringComponent and StringWhitespace types. + // + // The reason we introduce these is for the specific behaviors when formatting multiline + // string annotations. String annotations have a different desired width than tokens + // like arguments, comments, etc. + // + // Additionally, we don't wrap trailing whitespace until the preceding token is within + // the wrapping width, unlike other token types. + + vector result; + result.reserve(items.size()); + vector pending; + pending.reserve(items.size()); bool hasStrings = false; - for (auto& i : items) - { - if (i.type == StringSeparator && !i.tokens.empty()) - { - // We try to push separators onto a preceding word, otherwise treat as - // a singular atom - if (pending.empty()) - { - result.push_back(Item {Atom, {}, {i.tokens}, 0}); - } - else - { - for (auto& j : i.tokens) - pending.back().AddTokenToLastAtom(j); - result.push_back(Item {StringComponent, pending, {}, 0}); - } - pending.clear(); - hasStrings = true; - } - else if (i.type == StringWhitespace) - { - // Special case because we let whitespace trail even if over width - if (!pending.empty()) - { - result.push_back(Item {StringComponent, pending, {}, 0}); - pending.clear(); - } - result.push_back(Item {StringWhitespace, i.items, i.tokens, i.width}); - } - else if (i.type == FormatSpecifier || i.type == EscapeSequence) - { - // Flush previous tokens before special sequences like format specifiers or - // escape sequences - if (!pending.empty()) - { - result.push_back(Item {StringComponent, pending, {}, 0 }); - pending.clear(); - } - result.push_back(Item { Atom, i.items, i.tokens, i.width}); - } - else if (i.type == StartOfContainer && pending.empty()) - { - result.push_back(i); - } - else if (i.type == EndOfContainer && hasStrings && !pending.empty()) - { - result.push_back(Item {StringComponent, pending, {}, 0}); - result.push_back(i); - } - else - { - pending.push_back(Item {i.type, CreateStringGroups(i.items), i.tokens, 0}); - } + // flush pending into one StringComponent + auto flushString = [&]() { + if (!pending.empty()) { + result.emplace_back( + Item{ StringComponent, + std::move(pending), + {}, + 0 } + ); + pending.clear(); + } + }; + + for (auto const& orig : items) { + Item i = orig; + + if (i.type == StringSeparator && !i.tokens.empty()) { + if (pending.empty()) { + result.emplace_back( + Item{ StringComponent, + vector{}, + std::move(i.tokens), + 0 } + ); + } else { + for (auto& t : i.tokens) + pending.back().AddTokenToLastStringComponent(std::move(t)); + flushString(); + } + hasStrings = true; + } + else if (i.type == StringWhitespace) { + flushString(); + result.emplace_back( + Item{ StringWhitespace, + std::move(i.items), + std::move(i.tokens), + 0 } + ); + } + else if (i.type == FormatSpecifier || i.type == EscapeSequence) { + flushString(); + result.emplace_back( + Item{ StringComponent, + std::move(i.items), + std::move(i.tokens), + 0 } + ); + } + else if (i.type == StartOfContainer && pending.empty()) { + result.emplace_back(std::move(i)); + } + else if (i.type == EndOfContainer && hasStrings && !pending.empty()) { + result.emplace_back( + Item{ Group, + std::move(pending), + vector{}, + 0 } + ); + result.emplace_back(std::move(i)); + pending.clear(); + } + else { + vector nested; + if (!i.items.empty()) + nested = CreateStringGroups(i.items); + pending.emplace_back( + Item{ i.type, + std::move(nested), + std::move(i.tokens), + 0 } + ); + } } - if (!pending.empty()) - { - if (hasStrings) - result.push_back(Item {StringComponent, pending, {}, 0}); - else - result.insert(result.end(), pending.begin(), pending.end()); - } + if (!pending.empty()) { + if (hasStrings) { + flushString(); + } else { + result.insert( + result.end(), + make_move_iterator(pending.begin()), + make_move_iterator(pending.end()) + ); + } + } - return result; + return result; } static vector CreateAssignmentOperatorGroups(const vector& items) { - vector result, pending; + vector result; + result.reserve(items.size()); + vector pending; + pending.reserve(items.size()); + bool hasOperators = false; + + auto flushStatement = [&]() + { + if (!pending.empty()) + { + result.emplace_back(Item {Statement, std::move(pending), {}, 0}); + pending.clear(); + } + }; + for (auto& i : items) { if (i.type == Operator && !i.tokens.empty()) @@ -437,15 +544,14 @@ static vector CreateAssignmentOperatorGroups(const vector& items) { if (pending.empty()) { - result.push_back(Item {Atom, {}, {i.tokens}, 0}); + result.emplace_back(Item {Atom, {}, std::move(i.tokens), 0}); } else { for (auto& j : i.tokens) pending.back().AddTokenToLastAtom(j); - result.push_back(Item {Statement, pending, {}, 0}); + flushStatement(); } - pending.clear(); hasOperators = true; continue; } @@ -453,24 +559,24 @@ static vector CreateAssignmentOperatorGroups(const vector& items) if (i.type == StartOfContainer && pending.empty()) { - result.push_back(i); + result.emplace_back(std::move(i)); } else if (i.type == EndOfContainer && hasOperators && !pending.empty()) { - result.push_back(Item {Group, pending, {}, 0}); - result.push_back(i); + result.emplace_back(Item {Group, std::move(pending), {}, 0}); + result.emplace_back(i); pending.clear(); } else { - pending.push_back(Item {i.type, CreateAssignmentOperatorGroups(i.items), i.tokens, 0}); + pending.emplace_back(Item {i.type, CreateAssignmentOperatorGroups(i.items), i.tokens, 0}); } } if (!pending.empty()) { if (hasOperators) - result.push_back(Item {Group, pending, {}, 0}); + result.emplace_back(Item {Group, std::move(pending), {}, 0}); else result.insert(result.end(), pending.begin(), pending.end()); } @@ -481,47 +587,92 @@ static vector CreateAssignmentOperatorGroups(const vector& items) static vector CreateArgumentItems(const vector& items, bool inContainer) { - vector result, pending; + vector result; + result.reserve(items.size()); + vector pending; + pending.reserve(items.size()); bool hasArgs = false; - for (auto& i : items) + + auto flushArgument = [&]() + { + if (!pending.empty()) + { + result.emplace_back( + Item{ + inContainer ? Argument : Group, + std::move(pending), + vector{}, + 0 + } + ); + pending.clear(); + } + }; + + for (auto const& orig: items) { + Item i = orig; + if (i.type == ArgumentSeparator) { if (pending.empty()) { - result.push_back(Item {Atom, {}, {i.tokens}, 0}); + result.emplace_back( + Item{ + Atom, + vector{}, + std::move(i.tokens), + 0 + } + ); } else { - for (auto& j : i.tokens) - pending.back().AddTokenToLastAtom(j); - result.push_back(Item {inContainer ? Argument : Group, pending, {}, 0}); + for (auto& t: i.tokens) + pending.back().AddTokenToLastAtom(std::move(t)); + flushArgument(); } - pending.clear(); hasArgs = true; } else if (i.type == StartOfContainer && pending.empty()) { - result.push_back(i); + result.emplace_back(std::move(i)); } else if (i.type == EndOfContainer && hasArgs && !pending.empty()) { - result.push_back(Item {inContainer ? Argument : Group, pending, {}, 0}); - result.push_back(i); - pending.clear(); + flushArgument(); + result.emplace_back(std::move(i)); } else { - pending.push_back(Item {i.type, CreateArgumentItems(i.items, i.type == Container), i.tokens, 0}); + vector nested; + if (!i.items.empty()) + nested = CreateArgumentItems(i.items, i.type == Container); + pending.emplace_back( + Item{ + i.type, + std::move(nested), + std::move(i.tokens), + 0 + } + ); } } if (!pending.empty()) { if (hasArgs) - result.push_back(Item {inContainer ? Argument : Group, pending, {}, 0}); + { + flushArgument(); + } else - result.insert(result.end(), pending.begin(), pending.end()); + { + result.insert( + result.end(), + make_move_iterator(pending.begin()), + make_move_iterator(pending.end()) + ); + } } return result; @@ -530,44 +681,84 @@ static vector CreateArgumentItems(const vector& items, bool inContai static vector CreateOperatorGroups(const vector& items) { - vector result, pending; + vector result; + result.reserve(items.size()); + vector pending; + pending.reserve(items.size()); bool hasOperators = false; - for (auto& i : items) + + auto flushOperator = [&]() { - if (i.type == Operator) + if (!pending.empty()) { if (pending.size() == 1) - result.push_back(pending[0]); - else if (!pending.empty()) - result.push_back(Item {Group, pending, {}, 0}); - result.push_back(i); + { + result.emplace_back(std::move(pending[0])); + } + else + { + result.emplace_back( + Item{ + Group, + std::move(pending), + {}, + 0 + } + ); + } pending.clear(); - hasOperators = true; - continue; } + }; - if (i.type == StartOfContainer && pending.empty()) + for (auto const& orig: items) + { + Item i = orig; + + if (i.type == Operator) + { + flushOperator(); + result.emplace_back(std::move(i)); + hasOperators = true; + } + else if (i.type == StartOfContainer && pending.empty()) { - result.push_back(i); + result.emplace_back(std::move(i)); } else if (i.type == EndOfContainer && hasOperators && pending.size() > 1) { - result.push_back(Item {Group, pending, {}, 0}); - result.push_back(i); - pending.clear(); + flushOperator(); + result.emplace_back(std::move(i)); } else { - pending.push_back(Item {i.type, CreateOperatorGroups(i.items), i.tokens, 0}); + vector nested; + if (!i.items.empty()) + nested = CreateOperatorGroups(i.items); + pending.emplace_back( + Item{ + i.type, + std::move(nested), + std::move(i.tokens), + 0 + } + ); } } if (!pending.empty()) { if (hasOperators && pending.size() > 1) - result.push_back(Item {Group, pending, {}, 0}); + { + flushOperator(); + } else - result.insert(result.end(), pending.begin(), pending.end()); + { + result.insert( + result.end(), + make_move_iterator(pending.begin()), + make_move_iterator(pending.end()) + ); + } } return result; @@ -595,7 +786,7 @@ static vector CreateOperatorPrecedenceGroups(const vector& items) vector result; result.reserve(items.size()); for (auto& i : items) - result.push_back({i.type, CreateOperatorPrecedenceGroups(i.items), i.tokens, 0}); + result.emplace_back(Item {i.type, CreateOperatorPrecedenceGroups(i.items), i.tokens, 0}); return result; } @@ -610,9 +801,9 @@ static vector CreateOperatorPrecedenceGroups(const vector& items) if (precedence == lowestPrecedence.value()) { if (pending.size() == 1) - result.push_back(pending[0]); + result.emplace_back(pending[0]); else if (!pending.empty()) - result.push_back(Item {Group, pending, {}, 0}); + result.emplace_back(Item {Group, std::move(pending), {}, 0}); else result.insert(result.end(), pending.begin(), pending.end()); pending.clear(); @@ -621,24 +812,24 @@ static vector CreateOperatorPrecedenceGroups(const vector& items) if (i->type == StartOfContainer && pending.empty()) { - result.push_back(*i); + result.emplace_back(*i); } else if (i->type == EndOfContainer && pending.size() > 1 && !result.empty()) { - result.push_back(Item {Group, pending, {}, 0}); - result.push_back(*i); + result.emplace_back(Item {Group, std::move(pending), {}, 0}); + result.emplace_back(*i); pending.clear(); } else { - pending.push_back(*i); + pending.emplace_back(*i); } } if (!pending.empty()) { if (pending.size() > 1 && !result.empty()) - result.push_back(Item {Group, pending, {}, 0}); + result.emplace_back(Item {Group, pending, {}, 0}); else result.insert(result.end(), pending.begin(), pending.end()); } @@ -647,7 +838,7 @@ static vector CreateOperatorPrecedenceGroups(const vector& items) vector processed; processed.reserve(result.size()); for (auto& i : result) - processed.push_back({i.type, CreateOperatorPrecedenceGroups(i.items), i.tokens, 0}); + processed.emplace_back(Item{i.type, CreateOperatorPrecedenceGroups(i.items), i.tokens, 0}); return processed; } @@ -674,7 +865,7 @@ static vector RelocateStartAndEndOfContainerItems(const vector& item vector containerItems(containerContents, i.items.end()); containerItems = RelocateStartAndEndOfContainerItems(containerItems); if (!containerItems.empty()) - result.push_back({Container, containerItems, {}, 0}); + result.emplace_back(Item {Container, containerItems, {}, 0}); } else if (i.type == EndOfContainer && !result.empty()) { @@ -683,7 +874,7 @@ static vector RelocateStartAndEndOfContainerItems(const vector& item } else { - result.push_back(Item {i.type, RelocateStartAndEndOfContainerItems(i.items), i.tokens, 0}); + result.emplace_back(Item {i.type, RelocateStartAndEndOfContainerItems(i.items), i.tokens, 0}); } } return result; @@ -711,7 +902,7 @@ vector GenericLineFormatter::FormatLines( if (totalLength <= settings.desiredLineLength || contentLength <= settings.minimumContentLength) { // Line fits, emit as-is - result.push_back(currentLine); + result.emplace_back(currentLine); continue; } @@ -748,20 +939,11 @@ vector GenericLineFormatter::FormatLines( size_t desiredStringWidth = settings.stringWrappingWidth; if (indentation < settings.desiredLineLength) { - size_t remainingStringWidth = desiredStringWidth - indentation; + size_t remainingStringWidth = settings.desiredLineLength - indentation; if (remainingStringWidth > desiredStringWidth) desiredStringWidth = remainingStringWidth; } - // Compute target width for continuation string wrapping lines - size_t desiredStringContinuationWidth = settings.stringWrappingWidth; - if (continuationIndentation < settings.desiredLineLength) - { - size_t remainingStringWidth = desiredStringContinuationWidth - continuationIndentation; - if (remainingStringWidth > desiredStringContinuationWidth) - desiredStringContinuationWidth = remainingStringWidth; - } - // Gather the indentation tokens at the beginning of the line vector indentationTokens = currentLine.GetAddressAndIndentationTokens(); size_t tokenIndex = indentationTokens.size(); @@ -779,42 +961,45 @@ vector GenericLineFormatter::FormatLines( { case BraceToken: // Beginning of string - if (tokenIndex + 1 < currentLine.tokens.size() - && currentLine.tokens[tokenIndex + 1].type == StringToken) + if (trimmedText == "\"" && tokenIndex + 1 <= currentLine.tokens.size() && currentLine.tokens[tokenIndex + 1].type == StringToken) { // Create a ContainerContents item and place it onto the item stack. This will hold anything // inside the container once the end of the container is found. - items.push_back(Item {Container, {}, {}, 0}); + items.emplace_back(Item {Container, {}, {}, 0}); itemStack.push(items); // Starting a new context items.clear(); - items.push_back(Item {StartOfContainer, {}, {token}, 0}); + items.emplace_back(Item {StartOfContainer, {}, {token}, 0}); } // End of string - else if (currentLine.tokens[tokenIndex].type == StringToken - && tokenIndex + 1 < currentLine.tokens.size() - && currentLine.tokens[tokenIndex + 1].type == BraceToken) + else if (trimmedText == "\"" && tokenIndex > 0 && currentLine.tokens[tokenIndex - 1].type == StringToken) { - // Create a ContainerContents item and place it onto the item stack. This will hold anything - // inside the container once the end of the container is found. - items.push_back(Item {Container, {}, {}, 0}); - itemStack.push(items); + items.emplace_back(Item {EndOfContainer, {}, {token}, 0}); + + if (itemStack.empty()) + break; + + // Go back up the item stack and add the items to the container + vector parent = itemStack.top(); + itemStack.pop(); + parent.back().items.insert(parent.back().items.end(), items.begin(), items.end()); + items = parent; } else if (trimmedText == "(" || trimmedText == "[" || trimmedText == "{") { // Create a ContainerContents item and place it onto the item stack. This will hold anything // inside the container once the end of the container is found. - items.push_back(Item {Container, {}, {}, 0}); + items.emplace_back(Item {Container, {}, {}, 0}); itemStack.push(items); // Starting a new context items.clear(); - items.push_back(Item {StartOfContainer, {}, {token}, 0}); + items.emplace_back(Item {StartOfContainer, {}, {token}, 0}); } else if (trimmedText == ")" || trimmedText == "]" || trimmedText == "}") { - items.push_back(Item {EndOfContainer, {}, {token}, 0}); + items.emplace_back(Item {EndOfContainer, {}, {token}, 0}); if (itemStack.empty()) break; @@ -832,49 +1017,54 @@ vector GenericLineFormatter::FormatLines( // these are used to create clickable items when things are referenced by the comment. Item comment {Comment, {}, {}, 0}; for (; tokenIndex < currentLine.tokens.size(); tokenIndex++) - comment.tokens.push_back(currentLine.tokens[tokenIndex]); - items.push_back(comment); + comment.tokens.emplace_back(currentLine.tokens[tokenIndex]); + items.emplace_back(comment); break; } case TextToken: if (trimmedText == ",") - items.push_back(Item {ArgumentSeparator, {}, {token}, 0}); + items.emplace_back(Item {ArgumentSeparator, {}, {token}, 0}); else if ((!trimmedText.empty() && trimmedText[0] == '.') || trimmedText == "->") - items.push_back(Item {FieldAccessor, {}, {token}, 0}); + items.emplace_back(Item {FieldAccessor, {}, {token}, 0}); else if (trimmedText == ";") - items.push_back(Item {StatementSeparator, {}, {token}, 0}); + items.emplace_back(Item {StatementSeparator, {}, {token}, 0}); else if (trimmedText == ":" && !items.empty()) items.back().AddTokenToLastAtom(token); else - items.push_back(Item {Atom, {}, {token}, 0}); + items.emplace_back(Item {Atom, {}, {token}, 0}); break; case OperationToken: if ((!trimmedText.empty() && trimmedText[0] == '.') || trimmedText == "->") - items.push_back(Item {FieldAccessor, {}, {token}, 0}); + items.emplace_back(Item {FieldAccessor, {}, {token}, 0}); else - items.push_back(Item {Operator, {}, {token}, 0}); + items.emplace_back(Item {Operator, {}, {token}, 0}); break; case StringToken: { - vector stringTokens = ParseStringToken(token, settings.maximumAnnotationLength); - for (auto subToken : stringTokens) + if (token.width > desiredWidth) { - string trimmedSubText = TrimString(subToken.text); - if (trimmedSubText.empty()) - items.push_back(Item {StringWhitespace, {}, {subToken}, 0}); - else if (trimmedSubText[0] == '%') - items.push_back(Item {FormatSpecifier, {}, {subToken}, 0}); - else if (!trimmedSubText.empty() && trimmedSubText[0] == '\\') - items.push_back(Item {EscapeSequence, {}, {subToken}, 0}); - else if (trimmedSubText[0] == ',' || trimmedSubText[0] == '.' || trimmedSubText[0] == ':' || trimmedSubText[0] == ';') - items.push_back(Item {StringSeparator, {}, {subToken}, 0}); - else - items.push_back(Item {Atom, {}, {subToken}, 0}); + vector stringTokens = ParseStringToken(token, settings.maximumAnnotationLength); + for (auto subToken : stringTokens) + { + string trimmedSubText = TrimString(subToken.text); + if (trimmedSubText.empty()) + items.emplace_back(Item {StringWhitespace, {}, {subToken}, 0}); + else if (trimmedSubText[0] == '%') + items.emplace_back(Item {FormatSpecifier, {}, {subToken}, 0}); + else if (!trimmedSubText.empty() && trimmedSubText[0] == '\\') + items.emplace_back(Item {EscapeSequence, {}, {subToken}, 0}); + else if (trimmedSubText[0] == ',' || trimmedSubText[0] == '.' || trimmedSubText[0] == ':' || trimmedSubText[0] == ';') + items.emplace_back(Item {StringSeparator, {}, {subToken}, 0}); + else + items.emplace_back(Item {Atom, {}, {subToken}, 0}); + } + break; } + items.emplace_back(Item {Atom, {}, {token}, 0}); break; } default: - items.push_back(Item {Atom, {}, {token}, 0}); + items.emplace_back(Item {Atom, {}, {token}, 0}); break; } } @@ -923,18 +1113,24 @@ vector GenericLineFormatter::FormatLines( bool firstTokenOfLine = true; stack layoutStack; - layoutStack.push({items, additionalContinuationIndentation, desiredWidth, desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); + layoutStack.push({items, additionalContinuationIndentation, desiredWidth, desiredContinuationWidth, desiredStringWidth, false}); - auto newLine = [&]() { + auto newLine = [&](const bool forString = false) { if (!firstTokenOfLine) { string lastTokenText = outputLine.tokens.back().text; string trimmedText = TrimTrailingWhitespace(lastTokenText); outputLine.tokens.back().width -= lastTokenText.size() - trimmedText.size(); outputLine.tokens.back().text = trimmedText; + if (forString && outputLine.tokens.back().type == StringToken) + { + outputLine.tokens.emplace_back(BraceToken, "\""); + outputLine.tokens.back().width = 1; + currentWidth += 1; + } } - result.push_back(outputLine); + result.emplace_back(outputLine); outputLine.tokens = indentationTokens; // Make sure any collapsible state indicators are set to padding so that the indicators don't @@ -945,10 +1141,18 @@ vector GenericLineFormatter::FormatLines( outToken.context = ContentCollapsiblePadding; } + if (forString) + { + outputLine.tokens.emplace_back(BraceToken, "\""); + currentWidth = 1; + desiredWidth = desiredContinuationWidth; + firstTokenOfLine = true; + return; + } + outputLine.tokens.emplace_back(TextToken, string(additionalContinuationIndentation, ' ')); currentWidth = 0; desiredWidth = desiredContinuationWidth; - desiredStringWidth = desiredStringContinuationWidth; firstTokenOfLine = true; }; @@ -962,7 +1166,6 @@ vector GenericLineFormatter::FormatLines( desiredWidth = layoutStackEntry.desiredWidth; desiredContinuationWidth = layoutStackEntry.desiredContinuationWidth; desiredStringWidth = layoutStackEntry.desiredStringWidth; - desiredStringContinuationWidth = layoutStackEntry.desiredStringContinuationWidth; // Check to see if the scope we are returning to needs a new line. This is used when an argument // spans multiple lines. The rest of the arguments are placed on separate lines from the long argument. @@ -971,14 +1174,33 @@ vector GenericLineFormatter::FormatLines( for (auto item = items.begin(); item != items.end();) { - if (item->type == StringComponent && currentWidth + item->width > desiredStringWidth) + if (currentWidth + item->width > desiredStringWidth && item->type == StringComponent) { - // If a string is too wide to fit on the current line, create a newline - // without additional indentation - newLine(); + auto next = item; + ++next; + if (currentWidth > 0) + { + if (next != items.end()) + { + layoutStack.push({vector(next, items.end()), additionalContinuationIndentation, + desiredWidth, desiredContinuationWidth, desiredStringWidth, false}); + } + + newLine(true); + if (desiredContinuationWidth < settings.minimumContentLength) + desiredContinuationWidth = settings.minimumContentLength; + + layoutStack.push({item->items, additionalContinuationIndentation, desiredWidth, + desiredContinuationWidth, desiredStringWidth, false}); + break; + } + + item->AppendAllTokens(outputLine.tokens, firstTokenOfLine); + currentWidth += item->width; + ++item; continue; } - if (currentWidth + item->width > desiredWidth && item->type != StringWhitespace) + if (currentWidth + item->width > desiredWidth && item->type != StringWhitespace && item->type != StringComponent) { // Current item is too wide to fit on the current line, will need to start a new line. // Whitespace is allowed to be too wide; we push it on as the preceding word is wrapped. @@ -997,7 +1219,7 @@ vector GenericLineFormatter::FormatLines( if (next != items.end()) { layoutStack.push({vector(next, items.end()), additionalContinuationIndentation, - desiredWidth, desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, true}); + desiredWidth, desiredContinuationWidth, desiredStringWidth, true}); } newLine(); @@ -1008,13 +1230,8 @@ vector GenericLineFormatter::FormatLines( else desiredContinuationWidth -= settings.tabWidth; - if (desiredStringContinuationWidth < settings.minimumContentLength + settings.tabWidth) - desiredStringContinuationWidth = settings.minimumContentLength; - else - desiredStringContinuationWidth -= settings.tabWidth; - layoutStack.push({item->items, additionalContinuationIndentation, desiredWidth, - desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); + desiredContinuationWidth, desiredStringWidth, false}); break; } @@ -1024,10 +1241,10 @@ vector GenericLineFormatter::FormatLines( if (next != items.end()) { layoutStack.push({vector(next, items.end()), additionalContinuationIndentation, - desiredWidth, desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); + desiredWidth, desiredContinuationWidth, desiredStringWidth, false}); } layoutStack.push({item->items, additionalContinuationIndentation, desiredWidth, - desiredContinuationWidth, desiredStringWidth, desiredStringContinuationWidth, false}); + desiredContinuationWidth, desiredStringWidth, false}); break; }