| // 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/strings/diff.h" |
| |
| #include <iostream> |
| #include <vector> |
| |
| #include "absl/strings/str_split.h" |
| #include "common/strings/split.h" |
| #include "common/util/iterator_range.h" |
| #include "external_libs/editscript.h" |
| |
| namespace verible { |
| |
| using diff::Edit; |
| using diff::Edits; |
| using diff::Operation; |
| |
| static char EditOperationToLineMarker(Operation op) { |
| switch (op) { |
| case Operation::DELETE: |
| return '-'; |
| case Operation::EQUALS: |
| return ' '; |
| case Operation::INSERT: |
| return '+'; |
| default: |
| return '?'; |
| } |
| } |
| |
| LineDiffs::LineDiffs(absl::string_view before, absl::string_view after) |
| : before_text(before), |
| after_text(after), |
| before_lines(SplitLinesKeepLineTerminator(before_text)), |
| after_lines(SplitLinesKeepLineTerminator(after_text)), |
| edits(diff::GetTokenDiffs(before_lines.begin(), before_lines.end(), |
| after_lines.begin(), after_lines.end())) {} |
| |
| template <typename Iter> |
| static std::ostream& PrintLineRange(std::ostream& stream, char op, Iter start, |
| Iter end) { |
| for (const auto& line : make_range(start, end)) { |
| stream << op << line; |
| } |
| return stream; |
| } |
| |
| std::ostream& LineDiffs::PrintEdit(std::ostream& stream, |
| const Edit& edit) const { |
| const char op = EditOperationToLineMarker(edit.operation); |
| if (edit.operation == Operation::INSERT) { |
| PrintLineRange(stream, op, after_lines.begin() + edit.start, |
| after_lines.begin() + edit.end); |
| if (after_lines[edit.end - 1].back() != '\n') stream << "\n"; |
| } else { |
| PrintLineRange(stream, op, before_lines.begin() + edit.start, |
| before_lines.begin() + edit.end); |
| if (before_lines[edit.end - 1].back() != '\n') stream << "\n"; |
| } |
| stream << std::flush; |
| return stream; |
| } |
| |
| std::ostream& operator<<(std::ostream& stream, const LineDiffs& diffs) { |
| for (const auto& edit : diffs.edits) { |
| diffs.PrintEdit(stream, edit); |
| } |
| return stream; |
| } |
| |
| LineNumberSet DiffEditsToAddedLineNumbers(const Edits& edits) { |
| LineNumberSet added_lines; |
| for (const auto& edit : edits) { |
| if (edit.operation == Operation::INSERT) { |
| // Add 1 to convert from 0-indexed to 1-indexed. |
| added_lines.Add( |
| {static_cast<int>(edit.start) + 1, static_cast<int>(edit.end) + 1}); |
| } |
| } |
| return added_lines; |
| } |
| |
| std::vector<diff::Edits> DiffEditsToPatchHunks(const diff::Edits& edits, |
| int common_context) { |
| const int split_threshold = common_context * 2; |
| std::vector<diff::Edits> hunks(1); // start with 1 empty destination vector |
| for (const diff::Edit& edit : edits) { |
| auto& current_hunk = hunks.back(); |
| if (edit.operation == Operation::EQUALS) { |
| const int edit_size = edit.end - edit.start; |
| if (current_hunk.empty()) { |
| // For "end-pieces" (in this case, the head), threshold should be |
| // common_context, not split_threshold. |
| if (edit_size > common_context) { |
| // Add the tail end of this edit. |
| current_hunk.push_back( |
| diff::Edit{edit.operation, edit.end - common_context, edit.end}); |
| } else { |
| // Add the whole edit. |
| current_hunk.push_back(edit); |
| } |
| } else { // !current_hunk.empty() |
| // We don't know what follows this edit, so this may still be oversized. |
| // A final pass will trim excess sizing of EQUALS edits in tail |
| // position. |
| if (edit_size > split_threshold) { |
| // Close off the current hunk. |
| current_hunk.push_back(diff::Edit{edit.operation, edit.start, |
| edit.start + common_context}); |
| // Start the next hunk. |
| hunks.push_back(diff::Edits{ |
| diff::Edit{edit.operation, edit.end - common_context, edit.end}}); |
| } else { |
| // Add the whole edit. |
| current_hunk.push_back(edit); |
| } |
| } |
| } else { // operation is INSERT or DELETE |
| current_hunk.push_back(edit); |
| } |
| } |
| |
| // The last hunk may have been started before knowing it was the last one. |
| // Remove if it is a no-op. |
| const auto& last_hunk = hunks.back(); // hunks is always non-empty |
| if (last_hunk.size() == 1 && |
| last_hunk.front().operation == Operation::EQUALS) { |
| // This last hunk's only element is an Operation::EQUALS (no-change), |
| // so remove it. |
| hunks.pop_back(); |
| } |
| |
| // Trim excess EQUALS tail edits in each hunk. |
| for (auto& hunk : hunks) { |
| auto& tail = hunk.back(); |
| if (tail.operation == Operation::EQUALS) { |
| if (tail.end - tail.start > common_context) { |
| tail.end = tail.start + common_context; |
| } |
| } |
| } |
| return hunks; |
| } |
| |
| void LineDiffsToUnifiedDiff(std::ostream& stream, const LineDiffs& linediffs, |
| unsigned common_context, absl::string_view file_a, |
| absl::string_view file_b) { |
| const std::vector<diff::Edits> chunks = |
| DiffEditsToPatchHunks(linediffs.edits, common_context); |
| |
| if (chunks.empty()) return; |
| |
| if (!file_a.empty()) { |
| if (file_b.empty()) { |
| stream << "--- a/" << file_a << std::endl; |
| stream << "+++ b/" << file_a << std::endl; |
| } else { |
| stream << "--- " << file_a << std::endl; |
| stream << "+++ " << file_b << std::endl; |
| } |
| } |
| |
| int added_lines_count = 0; |
| for (const auto& chunk : chunks) { |
| int chunk_before_lines_count = 0; |
| int chunk_added_lines_count = 0; |
| |
| for (size_t i = 0; i < chunk.size(); ++i) { |
| const auto& edit = chunk[i]; |
| |
| if (edit.operation == Operation::INSERT) { |
| chunk_added_lines_count += edit.end - edit.start; |
| } else if (edit.operation == Operation::DELETE) { |
| chunk_before_lines_count += edit.end - edit.start; |
| chunk_added_lines_count -= edit.end - edit.start; |
| } else { |
| chunk_before_lines_count += edit.end - edit.start; |
| } |
| } |
| const int chunk_after_lines_count = |
| chunk_before_lines_count + chunk_added_lines_count; |
| |
| // Chunk header |
| stream << "@@ -" << (chunk.front().start + 1); |
| if (chunk_before_lines_count > 1) stream << "," << chunk_before_lines_count; |
| stream << " +" << (chunk.front().start + added_lines_count + 1); |
| if (chunk_after_lines_count > 1) stream << "," << chunk_after_lines_count; |
| stream << " @@" << std::endl; |
| |
| added_lines_count += chunk_added_lines_count; |
| |
| for (const auto& edit : chunk) { |
| linediffs.PrintEdit(stream, edit); |
| |
| // Last line from either original or new text, and final '\n' is missing? |
| if ((edit.operation != Operation::INSERT && |
| size_t(edit.end) == linediffs.before_lines.size() && |
| linediffs.before_text.back() != '\n') || |
| (edit.operation == Operation::INSERT && |
| size_t(edit.end) == linediffs.after_lines.size() && |
| linediffs.after_text.back() != '\n')) { |
| stream << "\\ No newline at end of file" << std::endl; |
| } |
| } |
| } |
| } |
| |
| } // namespace verible |