| // 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 |