| // Copyright 2017-2020 The Verible Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "common/formatting/align.h" | |
| #include <algorithm> | |
| #include <functional> | |
| #include <iostream> | |
| #include <iterator> | |
| #include <map> | |
| #include <numeric> | |
| #include <vector> | |
| #include "absl/strings/str_join.h" | |
| #include "common/formatting/format_token.h" | |
| #include "common/formatting/token_partition_tree.h" | |
| #include "common/formatting/unwrapped_line.h" | |
| #include "common/strings/display_utils.h" | |
| #include "common/strings/range.h" | |
| #include "common/text/concrete_syntax_leaf.h" | |
| #include "common/text/concrete_syntax_tree.h" | |
| #include "common/text/token_info.h" | |
| #include "common/text/token_stream_view.h" | |
| #include "common/text/tree_utils.h" | |
| #include "common/util/algorithm.h" | |
| #include "common/util/container_iterator_range.h" | |
| #include "common/util/enum_flags.h" | |
| #include "common/util/iterator_range.h" | |
| #include "common/util/logging.h" | |
| #include "common/util/tree_operations.h" | |
| #include "common/util/vector_tree.h" | |
| #include "common/util/vector_tree_iterators.h" | |
| namespace verible { | |
| static const verible::EnumNameMap<AlignmentPolicy>& AlignmentPolicyNameMap() { | |
| static const verible::EnumNameMap<AlignmentPolicy> kAlignmentPolicyNameMap({ | |
| {"align", AlignmentPolicy::kAlign}, | |
| {"flush-left", AlignmentPolicy::kFlushLeft}, | |
| {"preserve", AlignmentPolicy::kPreserve}, | |
| {"infer", AlignmentPolicy::kInferUserIntent}, | |
| // etc. | |
| }); | |
| return kAlignmentPolicyNameMap; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, AlignmentPolicy policy) { | |
| return AlignmentPolicyNameMap().Unparse(policy, stream); | |
| } | |
| bool AbslParseFlag(absl::string_view text, AlignmentPolicy* policy, | |
| std::string* error) { | |
| return AlignmentPolicyNameMap().Parse(text, policy, error, "AlignmentPolicy"); | |
| } | |
| std::string AbslUnparseFlag(const AlignmentPolicy& policy) { | |
| std::ostringstream stream; | |
| stream << policy; | |
| return stream.str(); | |
| } | |
| static int EffectiveCellWidth(const FormatTokenRange& tokens) { | |
| if (tokens.empty()) return 0; | |
| VLOG(2) << __FUNCTION__; | |
| // Sum token text lengths plus required pre-spacings (except first token). | |
| // Note: LeadingSpacesLength() honors where original spacing when preserved. | |
| return std::accumulate( | |
| tokens.begin(), tokens.end(), -tokens.front().LeadingSpacesLength(), | |
| [](int total_width, const PreFormatToken& ftoken) { | |
| const int pre_width = ftoken.LeadingSpacesLength(); | |
| const int text_length = ftoken.token->text().length(); | |
| VLOG(2) << " +" << pre_width << " +" << text_length; | |
| // TODO(fangism): account for multi-line tokens like | |
| // block comments. | |
| return total_width + ftoken.LeadingSpacesLength() + text_length; | |
| }); | |
| } | |
| static int EffectiveLeftBorderWidth(const FormatTokenRange& tokens) { | |
| if (tokens.empty()) return 0; | |
| return tokens.front().before.spaces_required; | |
| } | |
| using ColumnsTreePath = SyntaxTreePath; | |
| struct AlignmentCell { | |
| // Slice of format tokens in this cell (may be empty range). | |
| FormatTokenRange tokens; | |
| // The width of this token excerpt that complies with minimum spacing. | |
| int compact_width = 0; | |
| // Width of the left-side spacing before this cell, which can be considered | |
| // as a space-only column, usually no more than 1 space wide. | |
| int left_border_width = 0; | |
| // Returns true when neither the cell nor its subcells contain any tokens. | |
| bool IsUnused() const { return (tokens.empty() && compact_width == 0); } | |
| // Returns true when the cell contains subcells with tokens. | |
| bool IsComposite() const { return (tokens.empty() && compact_width > 0); } | |
| int TotalWidth() const { return left_border_width + compact_width; } | |
| FormatTokenRange ConstTokensRange() const { | |
| return FormatTokenRange(tokens.begin(), tokens.end()); | |
| } | |
| void UpdateWidths() { | |
| compact_width = EffectiveCellWidth(ConstTokensRange()); | |
| left_border_width = EffectiveLeftBorderWidth(tokens); | |
| } | |
| }; | |
| using AlignmentRow = VectorTree<AlignmentCell>; | |
| using AlignmentMatrix = std::vector<AlignmentRow>; | |
| std::ostream& operator<<(std::ostream& stream, const AlignmentCell& cell) { | |
| if (!cell.tokens.empty()) { | |
| // See UnwrappedLine::AsCode for similar printing. | |
| stream << absl::StrJoin(cell.tokens, " ", | |
| [](std::string* out, const PreFormatToken& token) { | |
| absl::StrAppend(out, token.Text()); | |
| }); | |
| } | |
| return stream; | |
| } | |
| // Type of functions used to generate textual node representations that are | |
| // suitable for use in rectangular cell. | |
| // The function is called with a tree node as its only argument. It should | |
| // return a string containing the cell's text and a single character used as | |
| // a filler for cell's empty space. | |
| template <typename ValueType> | |
| using CellLabelGetterFunc = | |
| std::function<std::pair<std::string, char>(const VectorTree<ValueType>&)>; | |
| // Recursively creates a tree with cells textual data. Its main purpose is to | |
| // split multi-line cell labels and calculate how many lines have to be printed. | |
| // This is a helper function used in ColumsTreeFormatter. | |
| template <typename ValueType, typename Cell> | |
| static std::size_t CreateTextNodes( | |
| const VectorTree<ValueType>& src_node, VectorTree<Cell>* dst_node, | |
| const CellLabelGetterFunc<ValueType>& get_cell_label) { | |
| static constexpr std::size_t kMinCellWidth = 2; | |
| std::size_t depth = 0; | |
| std::size_t subtree_depth = 0; | |
| for (const auto& src_child : src_node.Children()) { | |
| const auto [text, filler] = get_cell_label(src_child); | |
| const std::vector<std::string> lines = absl::StrSplit(text, '\n'); | |
| auto* dst_child = dst_node; | |
| for (const auto& line : lines) { | |
| dst_child->Children().emplace_back( | |
| Cell{line, filler, std::max(line.size(), kMinCellWidth)}); | |
| dst_child = &dst_child->Children().back(); | |
| } | |
| depth = std::max(depth, lines.size()); | |
| subtree_depth = std::max( | |
| subtree_depth, CreateTextNodes(src_child, dst_child, get_cell_label)); | |
| } | |
| return depth + subtree_depth; | |
| } | |
| // Prints visualization of columns tree 'root' to a 'stream'. The 'root' node | |
| // itself is not visualized. The 'get_cell_label' callback is used to get the | |
| // cell label printed for each node. | |
| // | |
| // The label's text can contain multiple lines. Each line can contain up to 3 | |
| // fields separated by tab character ('\t'). The first field is aligned to the | |
| // left. The second field is either aligned to the right (when there are | |
| // 2 fields) or centered (when there are 3 fields). The third field is aligned | |
| // to the right. Empty space is filled with label's filler character. | |
| template <typename ValueType> | |
| static void ColumnsTreeFormatter( | |
| std::ostream& stream, const VectorTree<ValueType>& root, | |
| const CellLabelGetterFunc<ValueType>& get_cell_label) { | |
| if (root.Children().empty()) return; | |
| static constexpr absl::string_view kCellSeparator = "|"; | |
| struct Cell { | |
| std::string text; | |
| char filler = ' '; | |
| std::size_t width = 0; | |
| }; | |
| VectorTree<Cell> text_tree; | |
| const std::size_t depth = | |
| CreateTextNodes<ValueType, Cell>(root, &text_tree, get_cell_label); | |
| // Adjust cells width to fit all their children | |
| for (auto& node : VectorTreePostOrderTraversal(text_tree)) { | |
| // Include separator width in cell width | |
| node.Value().width += kCellSeparator.size(); | |
| if (is_leaf(node)) continue; | |
| const std::size_t children_width = | |
| std::accumulate(node.Children().begin(), node.Children().end(), 0, | |
| [](std::size_t width, const VectorTree<Cell>& child) { | |
| return width + child.Value().width; | |
| }); | |
| if (node.Value().width < children_width) { | |
| node.Value().width = children_width; | |
| } | |
| } | |
| // Adjust cells width to fill their parents | |
| for (auto& node : VectorTreePreOrderTraversal(text_tree)) { | |
| if (is_leaf(node)) continue; | |
| std::size_t children_width = | |
| std::accumulate(node.Children().begin(), node.Children().end(), 0, | |
| [](std::size_t width, const VectorTree<Cell>& child) { | |
| return width + child.Value().width; | |
| }); | |
| // There is at least one child; each cell minimum width is equal to: | |
| // CreateTextNodes::kMinCellWidth + kCellSeparator.size() | |
| CHECK_GT(children_width, 0); | |
| if (node.Value().width > children_width) { | |
| auto extra_width = node.Value().width - children_width; | |
| for (auto& child : node.Children()) { | |
| CHECK_GT(children_width, 0); | |
| const auto added_child_width = | |
| extra_width * child.Value().width / children_width; // NOLINT | |
| extra_width -= added_child_width; | |
| children_width -= child.Value().width; | |
| child.Value().width += added_child_width; | |
| } | |
| } | |
| } | |
| std::vector<std::string> lines(depth); | |
| auto range = VectorTreePreOrderTraversal(text_tree); | |
| auto level_offset = NumAncestors(text_tree) + 1; | |
| for (auto& node : make_range(range.begin() + 1, range.end())) { | |
| auto& cell = node.Value(); | |
| const std::size_t level = NumAncestors(node) - level_offset; | |
| if (level > 0 && verible::IsFirstChild(node)) { | |
| const int padding_len = lines[level - 1].size() - lines[level].size() - | |
| node.Parent()->Value().width; | |
| if (padding_len > 0) { | |
| if (lines[level].empty()) | |
| lines[level].append(std::string(padding_len, ' ')); | |
| else if (padding_len > int(kCellSeparator.size())) | |
| lines[level].append(absl::StrCat( | |
| kCellSeparator, | |
| std::string(padding_len - kCellSeparator.size(), ' '))); | |
| } | |
| } | |
| const std::vector<absl::string_view> parts = | |
| absl::StrSplit(cell.text, '\t'); | |
| CHECK_LE(parts.size(), 3); | |
| const auto width = cell.width - kCellSeparator.size(); | |
| switch (parts.size()) { | |
| case 1: { | |
| const std::string pad(width - parts[0].size(), cell.filler); | |
| absl::StrAppend(&lines[level], kCellSeparator, parts[0], pad); | |
| break; | |
| } | |
| case 2: { | |
| const std::string pad(width - parts[0].size() - parts[1].size(), | |
| cell.filler); | |
| absl::StrAppend(&lines[level], kCellSeparator, parts[0], pad, | |
| parts.back()); | |
| break; | |
| } | |
| case 3: { | |
| std::size_t pos = | |
| std::clamp((width - parts[1].size()) / 2, parts[0].size() + 1, | |
| width - parts[2].size() - parts[1].size() - 1); | |
| const std::string left_pad(pos - parts[0].size(), cell.filler); | |
| const std::string right_pad( | |
| width - parts[2].size() - (pos + parts[1].size()), cell.filler); | |
| absl::StrAppend(&lines[level], kCellSeparator, parts[0], left_pad, | |
| parts[1], right_pad, parts[2]); | |
| break; | |
| } | |
| } | |
| } | |
| for (const auto& line : lines) { | |
| if (!line.empty()) stream << line << kCellSeparator << "\n"; | |
| } | |
| } | |
| // These properties are calculated/aggregated from alignment cells. | |
| struct AlignedColumnConfiguration { | |
| int width = 0; | |
| int left_border = 0; | |
| int TotalWidth() const { return left_border + width; } | |
| void UpdateFromCell(const AlignmentCell& cell) { | |
| width = std::max(width, cell.compact_width); | |
| left_border = std::max(left_border, cell.left_border_width); | |
| } | |
| }; | |
| /* static */ ColumnPositionTree* ColumnSchemaScanner::ReserveNewColumn( | |
| ColumnPositionTree* parent_column, const Symbol& symbol, | |
| const AlignmentColumnProperties& properties, const SyntaxTreePath& path) { | |
| CHECK_NOTNULL(parent_column); | |
| // The path helps establish a total ordering among all desired alignment | |
| // points, given that they may come from optional or repeated language | |
| // constructs. | |
| const SyntaxTreeLeaf* leaf = GetLeftmostLeaf(symbol); | |
| // It is possible for a node to be empty, in which case, ignore. | |
| if (leaf == nullptr) return nullptr; | |
| if (parent_column->Parent() != nullptr && parent_column->Children().empty()) { | |
| // Starting token of a column and its first subcolumn must be the same. | |
| // (subcolumns overlap their parent column). | |
| CHECK_EQ(parent_column->Value().starting_token, leaf->get()); | |
| } | |
| // It's possible the previous cell's path was intentionally altered | |
| // to effectively fuse it with the cell that is about to be added. | |
| // When this occurs, take the (previous) leftmost token, and suppress | |
| // adding a new column. | |
| if (parent_column->Children().empty() || | |
| parent_column->Children().back().Value().path != path) { | |
| parent_column->Children().emplace_back( | |
| ColumnPositionEntry{path, leaf->get(), properties}); | |
| const auto& column = parent_column->Children().back(); | |
| ColumnsTreePath column_path; | |
| verible::Path(column, column_path); | |
| VLOG(2) << "reserving new column for " << TreePathFormatter(path) << " at " | |
| << TreePathFormatter(column_path); | |
| } | |
| return &parent_column->Children().back(); | |
| } | |
| struct AggregateColumnData { | |
| AggregateColumnData() = default; | |
| // This is taken as the first seen set of properties in any given column. | |
| AlignmentColumnProperties properties; | |
| // These tokens's positions will be used to identify alignment cell | |
| // boundaries. | |
| std::vector<TokenInfo> starting_tokens; | |
| SyntaxTreePath path; | |
| void Import(const ColumnPositionEntry& cell) { | |
| if (starting_tokens.empty()) { | |
| path = cell.path; | |
| // Take the first set of properties, and ignore the rest. | |
| // They should be consistent, coming from alignment cell scanners, | |
| // but this is not verified. | |
| properties = cell.properties; | |
| } | |
| starting_tokens.push_back(cell.starting_token); | |
| } | |
| }; | |
| class ColumnSchemaAggregator { | |
| public: | |
| void Collect(const ColumnPositionTree& columns) { | |
| CollectColumnsTree(columns, &columns_); | |
| } | |
| // Sort columns by syntax tree path assigned to them and create an index that | |
| // maps syntax tree path to a column. Call this after collecting all columns. | |
| void Finalize() { | |
| syntax_to_columns_map_.clear(); | |
| for (auto& node : VectorTreePreOrderTraversal(columns_)) { | |
| if (node.Parent()) { | |
| // Index the column | |
| auto it = syntax_to_columns_map_.emplace_hint( | |
| syntax_to_columns_map_.end(), node.Value().path, ColumnsTreePath{}); | |
| verible::Path(node, it->second); | |
| } | |
| if (!is_leaf(node)) { | |
| // Sort subcolumns. This puts negative paths (leading non-tree token | |
| // columns) before empty, zero, and positive ones. | |
| std::sort(node.Children().begin(), node.Children().end(), | |
| [](const auto& a, const auto& b) { | |
| return a.Value().path < b.Value().path; | |
| }); | |
| // Propagate left_border_override property to the left subcolumn | |
| auto& left_child_data = node.Children().front().Value(); | |
| left_child_data.properties.left_border_override = | |
| std::max(left_child_data.properties.left_border_override, | |
| node.Value().properties.left_border_override); | |
| } | |
| } | |
| } | |
| const std::map<SyntaxTreePath, ColumnsTreePath>& SyntaxToColumnsMap() const { | |
| return syntax_to_columns_map_; | |
| } | |
| const VectorTree<AggregateColumnData>& Columns() const { return columns_; } | |
| VectorTree<AlignmentColumnProperties> ColumnProperties() const { | |
| return Transform<VectorTree<AlignmentColumnProperties>>( | |
| columns_, [](const VectorTree<AggregateColumnData>& data_node) { | |
| return data_node.Value().properties; | |
| }); | |
| } | |
| private: | |
| void CollectColumnsTree(const ColumnPositionTree& column, | |
| VectorTree<AggregateColumnData>* aggregate_column) { | |
| CHECK_NOTNULL(aggregate_column); | |
| for (const auto& subcolumn : column.Children()) { | |
| const auto [index_entry, insert] = | |
| syntax_to_columns_map_.try_emplace(subcolumn.Value().path); | |
| VectorTree<verible::AggregateColumnData>* aggregate_subcolumn; | |
| if (insert) { | |
| aggregate_column->Children().emplace_back(); | |
| aggregate_subcolumn = &aggregate_column->Children().back(); | |
| // Put aggregate column node's path in created index entry | |
| verible::Path(*aggregate_subcolumn, index_entry->second); | |
| } else { | |
| // Fact: existing aggregate_subcolumn is a direct child of | |
| // aggregate_column | |
| CHECK_GT(static_cast<int>(aggregate_column->Children().size()), | |
| index_entry->second.back()); | |
| aggregate_subcolumn = | |
| &aggregate_column->Children()[index_entry->second.back()]; | |
| } | |
| aggregate_subcolumn->Value().Import(subcolumn.Value()); | |
| CollectColumnsTree(subcolumn, aggregate_subcolumn); | |
| } | |
| } | |
| // Keeps track of unique positions where new columns are desired. | |
| // The nodes are sets of starting tokens, from which token ranges will be | |
| // computed per cell. | |
| VectorTree<AggregateColumnData> columns_; | |
| // 1:1 map between syntax tree's path and columns tree's path | |
| std::map<SyntaxTreePath, ColumnsTreePath> syntax_to_columns_map_; | |
| }; | |
| // CellLabelGetterFunc which creates a label with column's path relative to | |
| // its parent column and either '<' or '>' filler characters indicating whether | |
| // the column flushes to the left or the right. | |
| // `T` should be either AggregateColumnData or ColumnPositionEntry. | |
| template <typename T> | |
| static std::pair<std::string, char> GetColumnDataCellLabel( | |
| const VectorTree<T>& node) { | |
| std::ostringstream label; | |
| const SyntaxTreePath& path = node.Value().path; | |
| auto begin = path.begin(); | |
| if (node.Parent()) { | |
| // Find and skip common prefix | |
| const auto& parent_path = node.Parent()->Value().path; | |
| auto parent_begin = parent_path.begin(); | |
| while (begin != path.end() && parent_begin != parent_path.end() && | |
| *begin == *parent_begin) { | |
| ++begin; | |
| ++parent_begin; | |
| } | |
| } | |
| label << " \t "; | |
| if (begin != path.begin() && begin != path.end()) label << "."; | |
| label << SequenceFormatter( | |
| iterator_range<SyntaxTreePath::const_iterator>(begin, path.end()), "."); | |
| label << " \t "; | |
| return {label.str(), node.Value().properties.flush_left ? '<' : '>'}; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, | |
| const VectorTree<AggregateColumnData>& tree) { | |
| ColumnsTreeFormatter<AggregateColumnData>( | |
| stream, tree, GetColumnDataCellLabel<AggregateColumnData>); | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, const ColumnPositionTree& tree) { | |
| ColumnsTreeFormatter<ColumnPositionEntry>( | |
| stream, tree, GetColumnDataCellLabel<ColumnPositionEntry>); | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, | |
| const VectorTree<AlignmentCell>& tree) { | |
| ColumnsTreeFormatter<AlignmentCell>( | |
| stream, tree, | |
| [](const VectorTree<AlignmentCell>& node) | |
| -> std::pair<std::string, char> { | |
| const auto& cell = node.Value(); | |
| if (cell.IsUnused()) { | |
| return {"", '.'}; | |
| } else { | |
| const auto width_info = absl::StrCat("\t(", cell.left_border_width, | |
| "+", cell.compact_width, ")\t"); | |
| if (cell.IsComposite()) | |
| return {absl::StrCat("/", width_info, "\\"), '`'}; | |
| std::ostringstream label; | |
| label << "\t" << cell << "\t\n" << width_info; | |
| return {label.str(), ' '}; | |
| } | |
| }); | |
| return stream; | |
| } | |
| std::ostream& operator<<(std::ostream& stream, | |
| const VectorTree<AlignedColumnConfiguration>& tree) { | |
| ColumnsTreeFormatter<AlignedColumnConfiguration>( | |
| stream, tree, [](const VectorTree<AlignedColumnConfiguration>& node) { | |
| const auto& cell = node.Value(); | |
| const auto label = | |
| absl::StrCat("\t", cell.left_border, "+", cell.width, "\t"); | |
| return std::pair<std::string, char>{label, ' '}; | |
| }); | |
| return stream; | |
| } | |
| struct AlignmentRowData { | |
| // Range of format tokens whose space is to be adjusted for alignment. | |
| FormatTokenRange ftoken_range; | |
| // Set of cells found that correspond to an ordered, sparse set of columns | |
| // to be aligned with other rows. | |
| ColumnPositionTree sparse_columns; | |
| }; | |
| static void FillAlignmentRow( | |
| const AlignmentRowData& row_data, | |
| const std::map<SyntaxTreePath, ColumnsTreePath>& columns_map, | |
| AlignmentRow* row) { | |
| const auto& sparse_columns(row_data.sparse_columns); | |
| FormatTokenRange remaining_tokens_range(row_data.ftoken_range); | |
| FormatTokenRange* prev_cell_tokens = nullptr; | |
| if (!is_leaf(sparse_columns)) { | |
| for (const auto& col : VectorTreeLeavesTraversal(sparse_columns)) { | |
| const auto column_loc_iter = columns_map.find(col.Value().path); | |
| CHECK(column_loc_iter != columns_map.end()); | |
| const auto token_iter = std::find_if( | |
| remaining_tokens_range.begin(), remaining_tokens_range.end(), | |
| [=](const PreFormatToken& ftoken) { | |
| return BoundsEqual(ftoken.Text(), | |
| col.Value().starting_token.text()); | |
| }); | |
| CHECK(token_iter != remaining_tokens_range.end()); | |
| remaining_tokens_range.set_begin(token_iter); | |
| if (prev_cell_tokens != nullptr) prev_cell_tokens->set_end(token_iter); | |
| AlignmentRow& row_cell = verible::DescendPath( | |
| *row, column_loc_iter->second.begin(), column_loc_iter->second.end()); | |
| row_cell.Value().tokens = remaining_tokens_range; | |
| prev_cell_tokens = &row_cell.Value().tokens; | |
| } | |
| } | |
| } | |
| // Recursively calculates widths of each cell's subcells and, if needed, updates | |
| // cell's width to fit all subcells. | |
| static void UpdateAndPropagateRowCellWidths(AlignmentRow* node) { | |
| node->Value().UpdateWidths(); | |
| if (is_leaf(*node)) return; | |
| int total_width = 0; | |
| for (auto& child : node->Children()) { | |
| UpdateAndPropagateRowCellWidths(&child); | |
| total_width += child.Value().TotalWidth(); | |
| } | |
| if (node->Value().tokens.empty()) { | |
| node->Value().left_border_width = | |
| node->Children().front().Value().left_border_width; | |
| node->Value().compact_width = total_width - node->Value().left_border_width; | |
| } | |
| } | |
| static void ComputeRowCellWidths(AlignmentRow* row) { | |
| VLOG(2) << __FUNCTION__; | |
| UpdateAndPropagateRowCellWidths(row); | |
| // Force leftmost table border to be 0 because these cells start new lines | |
| // and thus should not factor into alignment calculation. | |
| // Note: this is different from how StateNode calculates column positions. | |
| auto* front = row; | |
| while (!front->Children().empty()) { | |
| front = &front->Children().front(); | |
| front->Value().left_border_width = 0; | |
| } | |
| VLOG(2) << "end of " << __FUNCTION__; | |
| } | |
| using AlignedFormattingColumnSchema = VectorTree<AlignedColumnConfiguration>; | |
| static AlignedFormattingColumnSchema ComputeColumnWidths( | |
| const AlignmentMatrix& matrix, | |
| const VectorTree<AlignmentColumnProperties>& column_properties) { | |
| VLOG(2) << __FUNCTION__; | |
| AlignedFormattingColumnSchema column_configs = | |
| Transform<AlignedFormattingColumnSchema>( | |
| matrix.front(), | |
| [](const AlignmentRow&) { return AlignedColumnConfiguration{}; }); | |
| // Check which cell before delimiter is the longest | |
| // If this cell is in the last row, the sizes of column with delimiter | |
| // must be set to 0 | |
| int longest_cell_before_delimiter = 0; | |
| bool align_to_last_row = false; | |
| for (const AlignmentRow& row : matrix) { | |
| auto column_prop_iter = | |
| VectorTreePreOrderTraversal(column_properties).begin(); | |
| const auto column_prop_end = | |
| VectorTreePreOrderTraversal(column_properties).end(); | |
| for (const auto& node : VectorTreePreOrderTraversal(row)) { | |
| const auto next_prop = std::next(column_prop_iter, 1); | |
| if (next_prop != column_prop_end && | |
| next_prop->Value().contains_delimiter) { | |
| if (longest_cell_before_delimiter < node.Value().TotalWidth()) { | |
| longest_cell_before_delimiter = node.Value().TotalWidth(); | |
| if (&row == &matrix.back()) align_to_last_row = true; | |
| } | |
| break; | |
| } | |
| ++column_prop_iter; | |
| } | |
| } | |
| for (const AlignmentRow& row : matrix) { | |
| auto column_iter = VectorTreePreOrderTraversal(column_configs).begin(); | |
| auto column_prop_iter = | |
| VectorTreePreOrderTraversal(column_properties).begin(); | |
| for (const auto& node : VectorTreePreOrderTraversal(row)) { | |
| if (column_prop_iter->Value().contains_delimiter && align_to_last_row) { | |
| column_iter->Value().width = 0; | |
| column_iter->Value().left_border = 0; | |
| } else { | |
| column_iter->Value().UpdateFromCell(node.Value()); | |
| if (column_prop_iter->Value().left_border_override != | |
| verible::AlignmentColumnProperties::kNoBorderOverride) { | |
| column_iter->Value().left_border = | |
| column_prop_iter->Value().left_border_override; | |
| } | |
| } | |
| ++column_iter; | |
| ++column_prop_iter; | |
| } | |
| } | |
| // Make sure columns are wide enough to fit all their subcolumns | |
| for (auto& column_iter : VectorTreePostOrderTraversal(column_configs)) { | |
| if (!is_leaf(column_iter)) { | |
| int children_width = std::accumulate( | |
| column_iter.Children().begin(), column_iter.Children().end(), 0, | |
| [](int width, const AlignedFormattingColumnSchema& node) { | |
| return width + node.Value().TotalWidth(); | |
| }); | |
| column_iter.Value().left_border = | |
| std::max(column_iter.Value().left_border, | |
| column_iter.Children().front().Value().left_border); | |
| column_iter.Value().width = | |
| std::max(column_iter.Value().width, | |
| children_width - column_iter.Value().left_border); | |
| } | |
| } | |
| VLOG(2) << "end of " << __FUNCTION__; | |
| return column_configs; | |
| } | |
| // Saved spacing mutation so that it can be examined before applying. | |
| // There is one of these for every format token that immediately follows an | |
| // alignment column boundary. | |
| struct DeferredTokenAlignment { | |
| // Points to the token to be modified. | |
| FormatTokenRange::iterator ftoken; | |
| // This is the spacing that would produce aligned formatting. | |
| int new_before_spacing; | |
| DeferredTokenAlignment(FormatTokenRange::iterator t, int spaces) | |
| : ftoken(t), new_before_spacing(spaces) {} | |
| // This value reflects an edit-distance (number of spaces) between aligned and | |
| // flushed-left formatting. | |
| int AlignVsFlushLeftSpacingDifference() const { | |
| return new_before_spacing - ftoken->before.spaces_required; | |
| } | |
| }; | |
| static void ComputeAlignedRowCellSpacings( | |
| const VectorTree<verible::AlignedColumnConfiguration>& column_configs, | |
| const VectorTree<verible::AlignmentColumnProperties>& properties, | |
| const AlignmentRow& row, std::vector<DeferredTokenAlignment>* align_actions, | |
| int* accrued_spaces) { | |
| ColumnsTreePath node_path; | |
| verible::Path(row, node_path); | |
| VLOG(2) << TreePathFormatter(node_path) << " " << __FUNCTION__ << std::endl; | |
| if (row.Children().empty()) return; | |
| auto column_config_it = column_configs.Children().begin(); | |
| auto column_properties_it = properties.Children().begin(); | |
| for (const auto& cell : row.Children()) { | |
| node_path.clear(); | |
| verible::Path(cell, node_path); | |
| if (cell.Value().IsUnused()) { | |
| const int total_width = column_config_it->Value().left_border + | |
| column_config_it->Value().width; | |
| VLOG(2) << TreePathFormatter(node_path) | |
| << " unused cell; width: " << total_width; | |
| *accrued_spaces += total_width; | |
| } else if (cell.Value().IsComposite()) { | |
| // Cummulative subcolumns width might be smaller than their parent | |
| // column's width. | |
| const int subcolumns_width = std::accumulate( | |
| column_config_it->Children().begin(), | |
| column_config_it->Children().end(), 0, | |
| [](int width, const VectorTree<AlignedColumnConfiguration>& node) { | |
| return width + node.Value().TotalWidth(); | |
| }); | |
| const int padding = | |
| column_config_it->Value().TotalWidth() - subcolumns_width; | |
| VLOG(2) << TreePathFormatter(node_path) << " composite cell" | |
| << "; padding: " << padding << "; flush: " | |
| << (column_properties_it->Value().flush_left ? "left" : "right"); | |
| if (!column_properties_it->Value().flush_left) *accrued_spaces += padding; | |
| ComputeAlignedRowCellSpacings(*column_config_it, *column_properties_it, | |
| cell, align_actions, accrued_spaces); | |
| if (column_properties_it->Value().flush_left) *accrued_spaces += padding; | |
| } else { | |
| *accrued_spaces += column_config_it->Value().left_border; | |
| VLOG(2) << TreePathFormatter(node_path) << " token cell" | |
| << "; starting token: " << cell.Value().tokens.front().Text(); | |
| // Align by setting the left-spacing based on sum of cell widths | |
| // before this one. | |
| const int padding = | |
| column_config_it->Value().width - cell.Value().compact_width; | |
| FormatTokenRange::iterator ftoken = cell.Value().tokens.begin(); | |
| int left_spacing; | |
| if (column_properties_it->Value().flush_left) { | |
| if (column_properties_it->Value().contains_delimiter) { | |
| left_spacing = 0; | |
| *accrued_spaces += padding; | |
| } else { | |
| left_spacing = *accrued_spaces; | |
| *accrued_spaces = padding; | |
| } | |
| } else { // flush right | |
| left_spacing = *accrued_spaces + padding; | |
| *accrued_spaces = 0; | |
| } | |
| align_actions->emplace_back(ftoken, left_spacing); | |
| VLOG(2) << TreePathFormatter(node_path) | |
| << " ... left_spacing: " << left_spacing; | |
| } | |
| ++column_config_it; | |
| ++column_properties_it; | |
| } | |
| } | |
| // Align cells by adjusting pre-token spacing for a single row. | |
| static std::vector<DeferredTokenAlignment> ComputeAlignedRowSpacings( | |
| const VectorTree<verible::AlignedColumnConfiguration>& column_configs, | |
| const VectorTree<verible::AlignmentColumnProperties>& properties, | |
| const AlignmentRow& row) { | |
| VLOG(2) << __FUNCTION__ << "; row:\n" << row; | |
| std::vector<DeferredTokenAlignment> align_actions; | |
| int accrued_spaces = 0; | |
| ComputeAlignedRowCellSpacings(column_configs, properties, row, &align_actions, | |
| &accrued_spaces); | |
| VLOG(2) << "end of " << __FUNCTION__; | |
| return align_actions; | |
| } | |
| static const AlignmentRow* RightmostSubcolumnWithTokens( | |
| const AlignmentRow& node) { | |
| if (!node.Value().tokens.empty()) return &node; | |
| for (const auto& child : reversed_view(node.Children())) { | |
| if (child.Value().TotalWidth() > 0) { | |
| return RightmostSubcolumnWithTokens(child); | |
| } | |
| } | |
| return nullptr; | |
| } | |
| static FormatTokenRange EpilogRange(const TokenPartitionTree& partition, | |
| const AlignmentRow& last_subcol) { | |
| // Identify the unaligned epilog tokens of this 'partition', i.e. those not | |
| // spanned by 'row'. | |
| auto partition_end = partition.Value().TokensRange().end(); | |
| auto row_end = last_subcol.Value().tokens.end(); | |
| return FormatTokenRange(row_end, partition_end); | |
| } | |
| // This width calculation accounts for the unaligned tokens in the tail position | |
| // of each aligned row (e.g. unaligned trailing comments). | |
| static bool AlignedRowsFitUnderColumnLimit( | |
| const std::vector<TokenPartitionIterator>& rows, | |
| const AlignmentMatrix& matrix, int total_column_width, int column_limit) { | |
| auto partition_iter = rows.begin(); | |
| for (const auto& row : matrix) { | |
| const auto* rightmost_subcolumn = RightmostSubcolumnWithTokens(row); | |
| if (rightmost_subcolumn) { | |
| // Identify the unaligned epilog text on each partition. | |
| const FormatTokenRange epilog_range( | |
| EpilogRange(**partition_iter, *rightmost_subcolumn)); | |
| const int aligned_partition_width = | |
| total_column_width + EffectiveCellWidth(epilog_range); | |
| if (aligned_partition_width > column_limit) { | |
| VLOG(1) << "Total aligned partition width " << aligned_partition_width | |
| << " exceeds limit " << column_limit | |
| << ", so not aligning this group."; | |
| return false; | |
| } | |
| } | |
| ++partition_iter; | |
| } | |
| return true; | |
| } | |
| // Holds alignment calculations for an alignable group of token partitions. | |
| struct AlignablePartitionGroup::GroupAlignmentData { | |
| // Contains alignment calculations. | |
| AlignmentMatrix matrix; | |
| // If this is empty, don't do any alignment. | |
| std::vector<std::vector<DeferredTokenAlignment>> align_actions_2D; | |
| int MaxAbsoluteAlignVsFlushLeftSpacingDifference() const { | |
| int result = std::numeric_limits<int>::min(); | |
| for (const auto& align_actions : align_actions_2D) { | |
| for (const auto& action : align_actions) { | |
| int abs_diff = std::abs(action.AlignVsFlushLeftSpacingDifference()); | |
| result = std::max(abs_diff, result); | |
| } | |
| } | |
| return result; | |
| } | |
| AlignmentPolicy InferUserIntendedAlignmentPolicy( | |
| const TokenPartitionRange& partitions) const; | |
| }; | |
| AlignablePartitionGroup::GroupAlignmentData | |
| AlignablePartitionGroup::CalculateAlignmentSpacings( | |
| const std::vector<TokenPartitionIterator>& rows, | |
| const AlignmentCellScannerFunction& cell_scanner_gen, int column_limit) { | |
| VLOG(1) << __FUNCTION__; | |
| GroupAlignmentData result; | |
| // Alignment requires 2+ rows. | |
| if (rows.size() <= 1) return result; | |
| // Rows validation: | |
| // In many (but not all) cases, all rows' nodes have the same type. | |
| // TODO(fangism): plumb through an optional verification function. | |
| VLOG(2) << "Walking syntax subtrees for each row"; | |
| ColumnSchemaAggregator column_schema; | |
| std::vector<AlignmentRowData> alignment_row_data; | |
| alignment_row_data.reserve(rows.size()); | |
| // Simultaneously step through each node's tree, adding a column to the | |
| // schema if *any* row wants it. This captures optional and repeated | |
| // constructs. | |
| for (const auto& row : rows) { | |
| // Each row should correspond to an individual list element | |
| const UnwrappedLine& unwrapped_line = row->Value(); | |
| // Scan each token-range for cell boundaries based on syntax, | |
| // and establish partial ordering based on syntax tree paths. | |
| auto sparse_columns = cell_scanner_gen(*row); | |
| // Make sure columns are properly ordered. | |
| std::sort( | |
| sparse_columns.Children().begin(), sparse_columns.Children().end(), | |
| [](const auto& a, const auto& b) { | |
| return CompareSyntaxTreePath(a.Value().path, b.Value().path) < 0; | |
| }); | |
| const AlignmentRowData row_data{ | |
| // Extract the range of format tokens whose spacings should be adjusted. | |
| unwrapped_line.TokensRange(), std::move(sparse_columns)}; | |
| alignment_row_data.emplace_back(row_data); | |
| VLOG(2) << "Row sparse columns:\n" << row_data.sparse_columns; | |
| // Aggregate union of all column keys (syntax tree paths). | |
| column_schema.Collect(row_data.sparse_columns); | |
| } | |
| VLOG(2) << "Generating column schema from collected row data"; | |
| column_schema.Finalize(); | |
| VLOG(2) << "Column schema:\n" << column_schema.Columns(); | |
| // Populate a matrix of cells, where cells span token ranges. | |
| // Null cells (due to optional constructs) are represented by empty ranges, | |
| // effectively width 0. | |
| VLOG(2) << "Filling dense matrix from sparse representation"; | |
| result.matrix.resize(rows.size()); | |
| { | |
| auto row_data_iter = alignment_row_data.cbegin(); | |
| for (auto& row : result.matrix) { | |
| VLOG(3) << "Row tokens: " | |
| << StringSpanOfTokenRange( | |
| FormatTokenRange(row_data_iter->ftoken_range.begin(), | |
| row_data_iter->ftoken_range.end())); | |
| row = Transform<AlignmentRow>(column_schema.Columns(), | |
| [](const VectorTree<AggregateColumnData>&) { | |
| return AlignmentCell{}; | |
| }); | |
| FillAlignmentRow(*row_data_iter, column_schema.SyntaxToColumnsMap(), | |
| &row); | |
| ComputeRowCellWidths(&row); | |
| VLOG(2) << "Filled row:\n" << row; | |
| ++row_data_iter; | |
| } | |
| } | |
| // Extract other non-computed column properties. | |
| const auto column_properties = column_schema.ColumnProperties(); | |
| // Compute max widths per column. | |
| VectorTree<AlignedColumnConfiguration> column_configs( | |
| ComputeColumnWidths(result.matrix, column_properties)); | |
| VLOG(2) << "Column widths:\n" << column_configs; | |
| { | |
| // Total width does not include initial left-indentation. | |
| // Assume indentation is the same for all partitions in each group. | |
| const int indentation = rows.front()->Value().IndentationSpaces(); | |
| const int total_column_width = | |
| indentation + column_configs.Value().TotalWidth(); | |
| VLOG(2) << "Total (aligned) column width = " << total_column_width; | |
| // if the aligned columns would exceed the column limit, then refuse to | |
| // align for now. However, this check alone does not include text that | |
| // follows the last aligned column, like trailing comments and EOL comments. | |
| if (total_column_width > column_limit) { | |
| VLOG(1) << "Total aligned column width " << total_column_width | |
| << " exceeds limit " << column_limit | |
| << ", so not aligning this group."; | |
| return result; | |
| } | |
| // Also check for length of unaligned trailing tokens. | |
| if (!AlignedRowsFitUnderColumnLimit(rows, result.matrix, total_column_width, | |
| column_limit)) | |
| return result; | |
| } | |
| // TODO(fangism): implement overflow mitigation fallback strategies. | |
| // At this point, the proposed alignment/padding 'fits'. | |
| // Compute pre-token spacings of each row to align to the column configs. | |
| // Store the mutation set in a 2D structure that reflects the original token | |
| // partitions and alignment matrix representation. | |
| result.align_actions_2D.reserve(result.matrix.size()); | |
| for (const auto& row : result.matrix) { | |
| result.align_actions_2D.push_back( | |
| ComputeAlignedRowSpacings(column_configs, column_properties, row)); | |
| } | |
| return result; | |
| } | |
| // This applies pre-calculated alignment spacings to aligned groups of format | |
| // tokens. | |
| void AlignablePartitionGroup::ApplyAlignment( | |
| const GroupAlignmentData& align_data) const { | |
| auto row = alignable_rows_.begin(); | |
| for (const auto& align_actions : align_data.align_actions_2D) { | |
| (*row)->Children().clear(); | |
| VLOG(3) << __FUNCTION__ << " processing row: " << **row; | |
| if (!align_actions.empty()) { | |
| auto& node = **row; | |
| auto& line = node.Value(); | |
| auto ftokens = line.TokensRange(); | |
| line.SetPartitionPolicy(PartitionPolicyEnum::kAlreadyFormatted); | |
| verible::TokenPartitionTree* current_cell = nullptr; | |
| if (align_actions.front().ftoken != ftokens.begin()) { | |
| node.Children().emplace_back( | |
| UnwrappedLine(0, ftokens.begin(), PartitionPolicyEnum::kInline)); | |
| current_cell = &node.Children().back(); | |
| } | |
| for (const auto& action : align_actions) { | |
| if (current_cell) { | |
| current_cell->Value().SpanUpToToken(action.ftoken); | |
| VLOG(3) << "new cell: margin=" | |
| << current_cell->Value().IndentationSpaces() << ", tokens=[ " | |
| << StringSpanOfTokenRange(current_cell->Value().TokensRange()) | |
| << " ]"; | |
| } | |
| node.Children().emplace_back( | |
| UnwrappedLine(action.new_before_spacing, action.ftoken, | |
| PartitionPolicyEnum::kInline)); | |
| current_cell = &node.Children().back(); | |
| } | |
| if (current_cell) { | |
| current_cell->Value().SpanUpToToken(ftokens.end()); | |
| VLOG(3) << "new cell: margin=" | |
| << current_cell->Value().IndentationSpaces() << ", tokens=[ " | |
| << StringSpanOfTokenRange(current_cell->Value().TokensRange()) | |
| << " ]"; | |
| } | |
| } | |
| ++row; | |
| } | |
| } | |
| std::vector<TokenPartitionIterator> FilterAlignablePartitions( | |
| const TokenPartitionRange& range, | |
| const IgnoreAlignmentRowPredicate& ignore_partition_predicate) { | |
| // This partition range may contain partitions that should not be | |
| // considered for column alignment purposes, so filter those out. | |
| std::vector<TokenPartitionIterator> qualified_partitions; | |
| qualified_partitions.reserve(range.size()); | |
| // like std::copy_if, but we want the iterators, not their pointees. | |
| for (auto iter = range.begin(); iter != range.end(); ++iter) { | |
| if (!ignore_partition_predicate(*iter)) { | |
| VLOG(2) << "including partition: " << *iter; | |
| qualified_partitions.push_back(iter); | |
| } else { | |
| VLOG(2) << "excluding partition: " << *iter; | |
| } | |
| } | |
| return qualified_partitions; | |
| } | |
| ExtractAlignmentGroupsFunction ExtractAlignmentGroupsAdapter( | |
| const std::function<std::vector<TaggedTokenPartitionRange>( | |
| const TokenPartitionRange&)>& legacy_extractor, | |
| const IgnoreAlignmentRowPredicate& legacy_ignore_predicate, | |
| const AlignmentCellScannerFunction& alignment_cell_scanner, | |
| AlignmentPolicy alignment_policy) { | |
| return [legacy_extractor, legacy_ignore_predicate, alignment_cell_scanner, | |
| alignment_policy](const TokenPartitionRange& full_range) { | |
| // must copy the closures, not just reference, to ensure valid lifetime | |
| const std::vector<TaggedTokenPartitionRange> ranges( | |
| legacy_extractor(full_range)); | |
| std::vector<AlignablePartitionGroup> groups; | |
| groups.reserve(ranges.size()); | |
| for (const auto& range : ranges) { | |
| // Apply the same alignment scanner and policy to all alignment groups. | |
| // This ignores range.match_subtype. | |
| groups.emplace_back(AlignablePartitionGroup{ | |
| FilterAlignablePartitions(range.range, legacy_ignore_predicate), | |
| alignment_cell_scanner, alignment_policy}); | |
| if (groups.back().IsEmpty()) groups.pop_back(); | |
| } | |
| return groups; | |
| }; | |
| } | |
| static int MaxOfPositives2D(const std::vector<std::vector<int>>& values) { | |
| int result = 0; | |
| for (const auto& row : values) { | |
| for (const int delta : row) { | |
| // Only accumulate positive values. | |
| if (delta > result) result = delta; | |
| } | |
| } | |
| return result; | |
| } | |
| // Educated guess whether user wants alignment. | |
| AlignmentPolicy | |
| AlignablePartitionGroup::GroupAlignmentData::InferUserIntendedAlignmentPolicy( | |
| const TokenPartitionRange& partitions) const { | |
| // Heuristics are implemented as a sequence of priority-ordered rules. | |
| { | |
| // If the visual distance between aligned and flushed left is sufficiently | |
| // small (and thus less likely to compromise readability), just align the | |
| // code region. The lower this threshold value, the more conservative the | |
| // aligner will be about forcing alignment over blocks of code. | |
| constexpr int kForceAlignMaxThreshold = 2; // TODO(fangism): configurable | |
| const int align_flush_diff = MaxAbsoluteAlignVsFlushLeftSpacingDifference(); | |
| VLOG(2) << "align vs. flush diff = " << align_flush_diff; | |
| VLOG(2) << " vs. " << kForceAlignMaxThreshold << " (max threshold)"; | |
| if (align_flush_diff <= kForceAlignMaxThreshold) { | |
| VLOG(2) << " <= threshold, so force-align."; | |
| return AlignmentPolicy::kAlign; | |
| } | |
| } | |
| // Compute spacing distances between the original and flush-left spacing. | |
| // This can be interpreted as "errors relative to flush-left spacing". | |
| const std::vector<std::vector<int>> flush_left_spacing_deltas( | |
| FlushLeftSpacingDifferences(partitions)); | |
| const int max_excess_spaces = MaxOfPositives2D(flush_left_spacing_deltas); | |
| VLOG(2) << "max excess spaces = " << max_excess_spaces; | |
| { | |
| // If the worst spacing error relative to the original code is <= than | |
| // this threshold, then infer that the user intended code to be flush-left. | |
| constexpr int kFlushLeftMaxThreshold = 2; // TODO(fangism): configurable | |
| VLOG(2) << " vs. " << kFlushLeftMaxThreshold << " (max threshold)"; | |
| if (max_excess_spaces <= kFlushLeftMaxThreshold) { | |
| VLOG(2) << " <= threshold, so flush-left."; | |
| return AlignmentPolicy::kFlushLeft; | |
| } | |
| } | |
| { | |
| // If the user injects more than this number of spaces in excess anywhere in | |
| // this block of code, then trigger alignment. | |
| constexpr int kForceAlignMinThreshold = 4; // TODO(fangism): configurable | |
| // This must be greater than kFlushLeftMaxThreshold. | |
| VLOG(2) << " vs. " << kForceAlignMinThreshold << " (min threshold)"; | |
| if (max_excess_spaces >= kForceAlignMinThreshold) { | |
| VLOG(2) << " >= threshold, so align."; | |
| return AlignmentPolicy::kAlign; | |
| } | |
| } | |
| // When in doubt, preserve. | |
| return AlignmentPolicy::kPreserve; | |
| } | |
| // TODO(mglb): this eventually could be moved to token_partition_tree.cc. | |
| void FormatUsingOriginalSpacing(TokenPartitionRange partition_range) { | |
| for (auto& partition : partition_range) { | |
| VLOG(4) << "partition before:\n" | |
| << TokenPartitionTreePrinter(partition, true); | |
| partition.Children().clear(); | |
| auto tokens = partition.Value().TokensRange(); | |
| if (tokens.empty()) { | |
| partition.Value().SetPartitionPolicy( | |
| PartitionPolicyEnum::kAlreadyFormatted); | |
| VLOG(4) << "partition after:\n" | |
| << TokenPartitionTreePrinter(partition, true); | |
| continue; | |
| } | |
| // Emulate spacing preservation using kAlreadyFormatted and kInline | |
| // partitions | |
| const int indentation = partition.Value().IndentationSpaces(); | |
| auto line = UnwrappedLine(indentation, tokens.begin(), | |
| PartitionPolicyEnum::kAlreadyFormatted); | |
| partition.Children().emplace_back(line); | |
| if (tokens.size() > 1) { | |
| // First token | |
| VLOG(5) << "token: \"" | |
| << EscapeString(tokens.front().OriginalLeadingSpaces()) | |
| << EscapeString(tokens.front().Text()) << '\"'; | |
| auto slice = | |
| UnwrappedLine(0, tokens.begin(), PartitionPolicyEnum::kInline); | |
| slice.SpanNextToken(); | |
| partition.Children().back().Children().emplace_back(slice); | |
| // Remaining tokens | |
| for (auto it = tokens.begin() + 1; it != tokens.end(); ++it) { | |
| const auto& token = *it; | |
| const auto whitespace = token.OriginalLeadingSpaces(); | |
| VLOG(5) << "token: \"" << EscapeString(whitespace) | |
| << EscapeString(token.Text()) << '\"'; | |
| int spacing = whitespace.size(); | |
| std::size_t last_newline_pos = whitespace.find_last_of('\n'); | |
| if (last_newline_pos != absl::string_view::npos) { | |
| // Update end of current line. | |
| partition.Children().back().Value().SpanUpToToken(it); | |
| // Start a new line. | |
| // Newlines count does not matter here. All newlines in leading | |
| // whitespace of the first token in a line are always preserved. | |
| // For details, see FormatWhitespaceWithDisabledByteRanges() called | |
| // from Formatter::Emit(). | |
| // | |
| // TODO(mglb): consider using correctly adjusted indentation to make | |
| // all lines indented correctly. Something like: | |
| // indentation + (this line orig. indent) - (1st line orig. indent) | |
| const auto line = | |
| UnwrappedLine(0, it, PartitionPolicyEnum::kAlreadyFormatted); | |
| partition.Children().emplace_back(line); | |
| // Count only spaces after the last '\n'. | |
| spacing -= last_newline_pos + 1; | |
| } | |
| auto slice = UnwrappedLine(spacing, it, PartitionPolicyEnum::kInline); | |
| slice.SpanNextToken(); | |
| partition.Children().back().Children().emplace_back(slice); | |
| } | |
| } | |
| partition.Children().back().Value().SpanUpToToken(tokens.end()); | |
| if (partition.Children().size() == 1) { | |
| HoistOnlyChild(partition); | |
| } else { | |
| partition.Value().SetPartitionPolicy(PartitionPolicyEnum::kAlwaysExpand); | |
| } | |
| VLOG(4) << "partition after:\n" | |
| << TokenPartitionTreePrinter(partition, true); | |
| } | |
| } | |
| void AlignablePartitionGroup::Align(int column_limit) const { | |
| // Compute dry-run of alignment spacings if it is needed. | |
| AlignmentPolicy policy = alignment_policy_; | |
| VLOG(2) << "AlignmentPolicy: " << policy; | |
| GroupAlignmentData align_data; | |
| switch (policy) { | |
| case AlignmentPolicy::kAlign: | |
| case AlignmentPolicy::kInferUserIntent: | |
| align_data = CalculateAlignmentSpacings( | |
| alignable_rows_, alignment_cell_scanner_, column_limit); | |
| break; | |
| default: | |
| break; | |
| } | |
| const TokenPartitionRange partition_range(Range()); | |
| // If enabled, try to decide automatically based on heurstics. | |
| if (policy == AlignmentPolicy::kInferUserIntent) { | |
| policy = align_data.InferUserIntendedAlignmentPolicy(partition_range); | |
| VLOG(2) << "AlignmentPolicy (automatic): " << policy; | |
| } | |
| // Align or not, depending on user-elected or inferred policy. | |
| switch (policy) { | |
| case AlignmentPolicy::kAlign: { | |
| if (!align_data.align_actions_2D.empty()) { | |
| // This modifies format tokens' spacing values. | |
| ApplyAlignment(align_data); | |
| } | |
| break; | |
| } | |
| case AlignmentPolicy::kFlushLeft: | |
| // This is already the default behavior elsewhere. Nothing else to do. | |
| break; | |
| case AlignmentPolicy::kInferUserIntent: | |
| // InferUserIntendedAlignmentPolicy() above should have set the policy to | |
| // anything other. | |
| LOG(ERROR) << "Alignment policy should have been decided at this point. " | |
| "Defaulting to kPreserve."; | |
| [[fallthrough]]; | |
| case AlignmentPolicy::kPreserve: | |
| FormatUsingOriginalSpacing(partition_range); | |
| break; | |
| } | |
| } | |
| void TabularAlignTokens( | |
| int column_limit, absl::string_view full_text, | |
| const ByteOffsetSet& disabled_byte_ranges, | |
| const ExtractAlignmentGroupsFunction& extract_alignment_groups, | |
| TokenPartitionTree* partition_ptr) { | |
| VLOG(1) << __FUNCTION__; | |
| // Each subpartition is presumed to correspond to a list element or | |
| // possibly some other ignored element like comments. | |
| auto& partition = *partition_ptr; | |
| auto& subpartitions = partition.Children(); | |
| // Identify groups of partitions to align, separated by blank lines. | |
| const TokenPartitionRange subpartitions_range(subpartitions.begin(), | |
| subpartitions.end()); | |
| if (subpartitions_range.empty()) return; | |
| VLOG(1) << "extracting alignment partition groups..."; | |
| const std::vector<AlignablePartitionGroup> alignment_groups( | |
| extract_alignment_groups(subpartitions_range)); | |
| for (const auto& alignment_group : alignment_groups) { | |
| const TokenPartitionRange partition_range(alignment_group.Range()); | |
| if (partition_range.empty()) continue; | |
| if (AnyPartitionSubRangeIsDisabled(partition_range, full_text, | |
| disabled_byte_ranges)) { | |
| // Within an aligned group, if the group is partially disabled | |
| // due to incremental formatting, then leave the new lines | |
| // unformatted rather than falling back to compact-left formatting. | |
| // However, allow the first token to be correctly indented. | |
| FormatUsingOriginalSpacing(partition_range); | |
| continue; | |
| // TODO(fangism): instead of disabling the whole range, sub-partition | |
| // it one more level, and operate on those ranges, essentially treating | |
| // no-format ranges like alignment group boundaries. | |
| // Requires IntervalSet::Intersect operation. | |
| // TODO(b/159824483): attempt to detect and re-use pre-existing alignment | |
| } | |
| // Calculate alignment and possibly apply it depending on alignment policy. | |
| alignment_group.Align(column_limit); | |
| } | |
| VLOG(1) << "end of " << __FUNCTION__; | |
| } | |
| std::vector<TaggedTokenPartitionRange> | |
| GetSubpartitionsBetweenBlankLinesSingleTag( | |
| const TokenPartitionRange& full_range, int subtype) { | |
| std::vector<TokenPartitionRange> ranges( | |
| GetSubpartitionsBetweenBlankLines(full_range)); | |
| std::vector<TaggedTokenPartitionRange> result; | |
| result.reserve(ranges.size()); | |
| for (const auto& range : ranges) { | |
| result.emplace_back(range, subtype); | |
| } | |
| return result; | |
| } | |
| std::vector<TaggedTokenPartitionRange> GetPartitionAlignmentSubranges( | |
| const TokenPartitionRange& partitions, | |
| const std::function<AlignedPartitionClassification( | |
| const TokenPartitionTree&)>& partition_selector, | |
| int min_match_count) { | |
| std::vector<TaggedTokenPartitionRange> result; | |
| // Grab ranges of consecutive data declarations with >= 2 elements. | |
| int last_match_subtype = 0; | |
| int match_count = 0; | |
| auto last_range_start = partitions.begin(); | |
| for (auto iter = last_range_start; iter != partitions.end(); ++iter) { | |
| const AlignedPartitionClassification align_class = | |
| partition_selector(*iter); | |
| switch (align_class.action) { | |
| case AlignmentGroupAction::kIgnore: | |
| continue; | |
| case AlignmentGroupAction::kMatch: { | |
| if (match_count == 0) { | |
| // This is the start of a new range of interest. | |
| last_range_start = iter; | |
| last_match_subtype = align_class.match_subtype; | |
| } | |
| if (align_class.match_subtype != last_match_subtype) { | |
| // Mismatch in substype, so close the last range, | |
| // and open a new one. | |
| if (match_count >= min_match_count) { | |
| result.emplace_back(last_range_start, iter, last_match_subtype); | |
| } | |
| match_count = 0; | |
| last_range_start = iter; | |
| last_match_subtype = align_class.match_subtype; | |
| } | |
| ++match_count; | |
| break; | |
| } | |
| case AlignmentGroupAction::kNoMatch: { | |
| if (match_count >= min_match_count) { | |
| result.emplace_back(last_range_start, iter, last_match_subtype); | |
| } | |
| match_count = 0; // reset | |
| break; | |
| } | |
| } // switch | |
| } // for | |
| // Flush out the last range. | |
| if (match_count >= min_match_count) { | |
| result.emplace_back(last_range_start, partitions.end(), last_match_subtype); | |
| } | |
| return result; | |
| } | |
| } // namespace verible |