| // Copyright (C) 2008 The Android Open Source Project |
| // |
| // 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. |
| |
| package com.google.gerrit.client.patches; |
| |
| import static com.google.gerrit.client.patches.PatchLine.Type.CONTEXT; |
| import static com.google.gerrit.client.patches.PatchLine.Type.DELETE; |
| import static com.google.gerrit.client.patches.PatchLine.Type.INSERT; |
| |
| import com.google.gerrit.client.Gerrit; |
| import com.google.gerrit.common.data.CommentDetail; |
| import com.google.gerrit.common.data.PatchScript; |
| import com.google.gerrit.common.data.PatchScript.DisplayMethod; |
| import com.google.gerrit.common.data.PatchSetDetail; |
| import com.google.gerrit.prettify.client.SparseHtmlFile; |
| import com.google.gerrit.prettify.common.EditList; |
| import com.google.gerrit.prettify.common.EditList.Hunk; |
| import com.google.gerrit.reviewdb.client.PatchLineComment; |
| import com.google.gerrit.reviewdb.client.PatchSet; |
| import com.google.gwt.dom.client.Element; |
| import com.google.gwt.user.client.DOM; |
| import com.google.gwt.user.client.Event; |
| import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; |
| import com.google.gwt.user.client.ui.UIObject; |
| import com.google.gwtexpui.safehtml.client.SafeHtml; |
| import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| public class UnifiedDiffTable extends AbstractPatchContentTable { |
| private static final int PC = 3; |
| private static final Comparator<PatchLineComment> BY_DATE = |
| new Comparator<PatchLineComment>() { |
| @Override |
| public int compare(final PatchLineComment o1, final PatchLineComment o2) { |
| return o1.getWrittenOn().compareTo(o2.getWrittenOn()); |
| } |
| }; |
| |
| protected boolean isFileCommentBorderRowExist; |
| // Cursors. |
| protected int rowOfTableHeaderB; |
| protected int borderRowOfFileComment; |
| |
| @Override |
| protected void onCellDoubleClick(final int row, final int column) { |
| if (column > C_ARROW && getRowItem(row) instanceof PatchLine) { |
| final PatchLine pl = (PatchLine) getRowItem(row); |
| switch (pl.getType()) { |
| case DELETE: |
| case CONTEXT: |
| createCommentEditor(row + 1, PC, pl.getLineA(), (short) 0); |
| break; |
| case INSERT: |
| createCommentEditor(row + 1, PC, pl.getLineB(), (short) 1); |
| break; |
| case REPLACE: |
| break; |
| } |
| } |
| } |
| |
| @Override |
| protected void updateCursor(final PatchLineComment newComment) { |
| if (newComment.getLine() == R_HEAD) { |
| final PatchSet.Id psId = |
| newComment.getKey().getParentKey().getParentKey(); |
| switch (newComment.getSide()) { |
| case FILE_SIDE_A: |
| if (idSideA == null && idSideB.equals(psId)) { |
| rowOfTableHeaderB++; |
| borderRowOfFileComment++; |
| return; |
| } |
| break; |
| case FILE_SIDE_B: |
| if (idSideA != null && idSideA.equals(psId)) { |
| rowOfTableHeaderB++; |
| borderRowOfFileComment++; |
| } else if (idSideB.equals(psId)) { |
| borderRowOfFileComment++; |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected void onCellSingleClick(Event event, int row, int column) { |
| super.onCellSingleClick(event, row, column); |
| if (column == 1 || column == 2) { |
| if (!"".equals(table.getText(row, column))) { |
| onCellDoubleClick(row, column); |
| } |
| } |
| } |
| |
| @Override |
| protected void destroyCommentRow(final int row) { |
| super.destroyCommentRow(row); |
| if (this.rowOfTableHeaderB + 1 == row && row + 1 == borderRowOfFileComment) { |
| table.removeRow(row); |
| isFileCommentBorderRowExist = false; |
| } |
| } |
| |
| @Override |
| public void remove(CommentEditorPanel panel) { |
| super.remove(panel); |
| if (panel.getComment().getLine() == AbstractPatchContentTable.R_HEAD) { |
| final PatchSet.Id psId = |
| panel.getComment().getKey().getParentKey().getParentKey(); |
| switch (panel.getComment().getSide()) { |
| case FILE_SIDE_A: |
| if (idSideA == null && idSideB.equals(psId)) { |
| rowOfTableHeaderB--; |
| borderRowOfFileComment--; |
| return; |
| } |
| break; |
| case FILE_SIDE_B: |
| if (idSideA != null && idSideA.equals(psId)) { |
| rowOfTableHeaderB--; |
| borderRowOfFileComment--; |
| } else if (idSideB.equals(psId)) { |
| borderRowOfFileComment--; |
| } |
| } |
| } |
| } |
| |
| @Override |
| protected void onInsertComment(final PatchLine pl) { |
| final int row = getCurrentRow(); |
| switch (pl.getType()) { |
| case DELETE: |
| case CONTEXT: |
| createCommentEditor(row + 1, PC, pl.getLineA(), (short) 0); |
| break; |
| case INSERT: |
| createCommentEditor(row + 1, PC, pl.getLineB(), (short) 1); |
| break; |
| case REPLACE: |
| break; |
| } |
| } |
| |
| private void appendImgTag(SafeHtmlBuilder nc, String url) { |
| nc.openElement("img"); |
| nc.setAttribute("src", url); |
| nc.closeElement("img"); |
| } |
| |
| @Override |
| protected void createFileCommentEditorOnSideA() { |
| createCommentEditor(R_HEAD + 1, PC, R_HEAD, FILE_SIDE_A); |
| } |
| |
| @Override |
| protected void createFileCommentEditorOnSideB() { |
| createCommentEditor(rowOfTableHeaderB + 1, PC, R_HEAD, FILE_SIDE_B); |
| createFileCommentBorderRow(); |
| } |
| |
| private void populateTableHeader(final PatchScript script, |
| final PatchSetDetail detail) { |
| initHeaders(script, detail); |
| table.setWidget(R_HEAD, PC, headerSideA); |
| table.setWidget(rowOfTableHeaderB, PC, headerSideB); |
| table.getFlexCellFormatter().addStyleName(R_HEAD, PC, |
| Gerrit.RESOURCES.css().unifiedTableHeader()); |
| table.getFlexCellFormatter().addStyleName(rowOfTableHeaderB, PC, |
| Gerrit.RESOURCES.css().unifiedTableHeader()); |
| |
| // Add icons to lineNumber column header |
| if (headerSideA.isFileOrCommitMessage()) { |
| table.setWidget(R_HEAD, 1, iconA); |
| } |
| if (headerSideB.isFileOrCommitMessage()) { |
| table.setWidget(rowOfTableHeaderB, 2, iconB); |
| } |
| } |
| |
| private void allocateTableHeader(SafeHtmlBuilder nc) { |
| rowOfTableHeaderB = 1; |
| borderRowOfFileComment = 2; |
| for (int i = R_HEAD; i < borderRowOfFileComment; i++) { |
| openTableHeaderLine(nc); |
| padLineNumberOnTableHeaderForSideA(nc); |
| padLineNumberOnTableHeaderForSideB(nc); |
| nc.openTd(); |
| nc.setStyleName(Gerrit.RESOURCES.css().fileLine()); |
| nc.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); |
| nc.closeTd(); |
| closeLine(nc); |
| } |
| } |
| |
| @Override |
| protected void render(final PatchScript script, final PatchSetDetail detail) { |
| final SafeHtmlBuilder nc = new SafeHtmlBuilder(); |
| allocateTableHeader(nc); |
| |
| // Display the patch header |
| for (final String line : script.getPatchHeader()) { |
| appendFileHeader(nc, line); |
| } |
| final ArrayList<PatchLine> lines = new ArrayList<>(); |
| |
| if (hasDifferences(script)) { |
| if (script.getDisplayMethodA() == DisplayMethod.IMG |
| || script.getDisplayMethodB() == DisplayMethod.IMG) { |
| appendImageDifferences(script, nc); |
| } else if (!isDisplayBinary) { |
| appendTextDifferences(script, nc, lines); |
| } |
| } else { |
| appendNoDifferences(nc); |
| } |
| |
| resetHtml(nc); |
| populateTableHeader(script, detail); |
| if (hasDifferences(script)) { |
| initScript(script); |
| if (!isDisplayBinary) { |
| int row = script.getPatchHeader().size(); |
| final CellFormatter fmt = table.getCellFormatter(); |
| final Iterator<PatchLine> iLine = lines.iterator(); |
| while (iLine.hasNext()) { |
| final PatchLine l = iLine.next(); |
| final String n; |
| switch (l.getType()) { |
| case CONTEXT: |
| n = Gerrit.RESOURCES.css().diffTextCONTEXT(); |
| break; |
| case DELETE: |
| n = Gerrit.RESOURCES.css().diffTextDELETE(); |
| break; |
| case INSERT: |
| n = Gerrit.RESOURCES.css().diffTextINSERT(); |
| break; |
| default: |
| continue; |
| } |
| while (!fmt.getStyleName(row, PC).contains(n)) { |
| row++; |
| } |
| setRowItem(row++, l); |
| } |
| } |
| } |
| } |
| |
| private void appendImageLine(final SafeHtmlBuilder nc, final String url, |
| final boolean syntaxHighlighting, final boolean isInsert) { |
| nc.openTr(); |
| nc.setAttribute("valign", "center"); |
| nc.setAttribute("align", "center"); |
| |
| nc.openTd(); |
| nc.setStyleName(Gerrit.RESOURCES.css().iconCell()); |
| nc.closeTd(); |
| |
| padLineNumberForSideA(nc); |
| padLineNumberForSideB(nc); |
| |
| nc.openTd(); |
| nc.setStyleName(Gerrit.RESOURCES.css().fileLine()); |
| if (isInsert) { |
| setStyleInsert(nc, syntaxHighlighting); |
| } else { |
| setStyleDelete(nc, syntaxHighlighting); |
| } |
| appendImgTag(nc, url); |
| nc.closeTd(); |
| |
| nc.closeTr(); |
| } |
| |
| private void appendImageDifferences(final PatchScript script, |
| final SafeHtmlBuilder nc) { |
| final boolean syntaxHighlighting = |
| script.getDiffPrefs().isSyntaxHighlighting(); |
| if (script.getDisplayMethodA() == DisplayMethod.IMG) { |
| final String url = getUrlA(); |
| appendImageLine(nc, url, syntaxHighlighting, false); |
| } |
| if (script.getDisplayMethodB() == DisplayMethod.IMG) { |
| final String url = getUrlB(); |
| appendImageLine(nc, url, syntaxHighlighting, true); |
| } |
| } |
| |
| private void appendTextDifferences(final PatchScript script, |
| final SafeHtmlBuilder nc, final ArrayList<PatchLine> lines) { |
| final SparseHtmlFile a = getSparseHtmlFileA(script); |
| final SparseHtmlFile b = getSparseHtmlFileB(script); |
| final boolean syntaxHighlighting = |
| script.getDiffPrefs().isSyntaxHighlighting(); |
| for (final EditList.Hunk hunk : script.getHunks()) { |
| appendHunkHeader(nc, hunk); |
| while (hunk.next()) { |
| if (hunk.isContextLine()) { |
| openLine(nc); |
| appendLineNumberForSideA(nc, hunk.getCurA()); |
| appendLineNumberForSideB(nc, hunk.getCurB()); |
| appendLineText(nc, false, CONTEXT, a, hunk.getCurA()); |
| closeLine(nc); |
| hunk.incBoth(); |
| lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB())); |
| |
| } else if (hunk.isDeletedA()) { |
| openLine(nc); |
| appendLineNumberForSideA(nc, hunk.getCurA()); |
| padLineNumberForSideB(nc); |
| appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA()); |
| closeLine(nc); |
| hunk.incA(); |
| lines.add(new PatchLine(DELETE, hunk.getCurA(), -1)); |
| if (a.size() == hunk.getCurA() |
| && script.getA().isMissingNewlineAtEnd()) { |
| appendNoLF(nc); |
| } |
| |
| } else if (hunk.isInsertedB()) { |
| openLine(nc); |
| padLineNumberForSideA(nc); |
| appendLineNumberForSideB(nc, hunk.getCurB()); |
| appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB()); |
| closeLine(nc); |
| hunk.incB(); |
| lines.add(new PatchLine(INSERT, -1, hunk.getCurB())); |
| if (b.size() == hunk.getCurB() |
| && script.getB().isMissingNewlineAtEnd()) { |
| appendNoLF(nc); |
| } |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void display(final CommentDetail cd, boolean expandComments) { |
| if (cd == null || cd.isEmpty()) { |
| return; |
| } |
| setAccountInfoCache(cd.getAccounts()); |
| |
| final ArrayList<PatchLineComment> all = new ArrayList<>(); |
| for (int row = 0; row < table.getRowCount();) { |
| final List<PatchLineComment> fora; |
| final List<PatchLineComment> forb; |
| if (row == R_HEAD) { |
| fora = cd.getForA(R_HEAD); |
| forb = cd.getForB(R_HEAD); |
| row++; |
| |
| if (!fora.isEmpty()) { |
| row = insert(fora, row); |
| } |
| rowOfTableHeaderB = row; |
| borderRowOfFileComment = row + 1; |
| if (!forb.isEmpty()) { |
| row++;// Skip the Header of sideB. |
| row = insert(forb, row); |
| borderRowOfFileComment = row; |
| createFileCommentBorderRow(); |
| } |
| } else if (getRowItem(row) instanceof PatchLine) { |
| final PatchLine pLine = (PatchLine) getRowItem(row); |
| fora = cd.getForA(pLine.getLineA()); |
| forb = cd.getForB(pLine.getLineB()); |
| row++; |
| |
| if (!fora.isEmpty() && !forb.isEmpty()) { |
| all.clear(); |
| all.addAll(fora); |
| all.addAll(forb); |
| Collections.sort(all, BY_DATE); |
| row = insert(all, row); |
| |
| } else if (!fora.isEmpty()) { |
| row = insert(fora, row); |
| |
| } else if (!forb.isEmpty()) { |
| row = insert(forb, row); |
| } |
| } else { |
| row++; |
| continue; |
| } |
| } |
| } |
| |
| private void defaultStyle(final int row, final CellFormatter fmt) { |
| fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().lineNumber()); |
| fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().rightBorder()); |
| fmt.addStyleName(row, PC - 1, Gerrit.RESOURCES.css().lineNumber()); |
| fmt.addStyleName(row, PC, Gerrit.RESOURCES.css().diffText()); |
| } |
| |
| @Override |
| protected void insertRow(final int row) { |
| super.insertRow(row); |
| final CellFormatter fmt = table.getCellFormatter(); |
| defaultStyle(row, fmt); |
| } |
| |
| private int insert(final List<PatchLineComment> in, int row) { |
| for (Iterator<PatchLineComment> ci = in.iterator(); ci.hasNext();) { |
| final PatchLineComment c = ci.next(); |
| if (c.getLine() == R_HEAD) { |
| insertFileCommentRow(row); |
| } else { |
| insertRow(row); |
| } |
| bindComment(row, PC, c, !ci.hasNext()); |
| row++; |
| } |
| return row; |
| } |
| |
| @Override |
| protected void insertFileCommentRow(final int row) { |
| table.insertRow(row); |
| final CellFormatter fmt = table.getCellFormatter(); |
| |
| fmt.addStyleName(row, C_ARROW, // |
| Gerrit.RESOURCES.css().iconCellOfFileCommentRow()); |
| defaultStyle(row, fmt); |
| |
| fmt.addStyleName(row, C_ARROW, // |
| Gerrit.RESOURCES.css().cellsNextToFileComment()); |
| fmt.addStyleName(row, PC - 2, // |
| Gerrit.RESOURCES.css().cellsNextToFileComment()); |
| fmt.addStyleName(row, PC - 1, // |
| Gerrit.RESOURCES.css().cellsNextToFileComment()); |
| } |
| |
| private void createFileCommentBorderRow() { |
| if (!isFileCommentBorderRowExist) { |
| isFileCommentBorderRowExist = true; |
| table.insertRow(borderRowOfFileComment); |
| final CellFormatter fmt = table.getCellFormatter(); |
| fmt.addStyleName(borderRowOfFileComment, C_ARROW, // |
| Gerrit.RESOURCES.css().iconCellOfFileCommentRow()); |
| defaultStyle(borderRowOfFileComment, fmt); |
| |
| final Element iconCell = |
| fmt.getElement(borderRowOfFileComment, C_ARROW); |
| UIObject.setStyleName(DOM.getParent(iconCell), // |
| Gerrit.RESOURCES.css().fileCommentBorder(), true); |
| } |
| } |
| |
| private void appendFileHeader(final SafeHtmlBuilder m, final String line) { |
| openLine(m); |
| padLineNumberForSideA(m); |
| padLineNumberForSideB(m); |
| |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().fileLine()); |
| m.addStyleName(Gerrit.RESOURCES.css().diffText()); |
| m.addStyleName(Gerrit.RESOURCES.css().diffTextFileHeader()); |
| m.append(line); |
| m.closeTd(); |
| closeLine(m); |
| } |
| |
| private void appendHunkHeader(final SafeHtmlBuilder m, final Hunk hunk) { |
| openLine(m); |
| padLineNumberForSideA(m); |
| padLineNumberForSideB(m); |
| |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().fileLine()); |
| m.addStyleName(Gerrit.RESOURCES.css().diffText()); |
| m.addStyleName(Gerrit.RESOURCES.css().diffTextHunkHeader()); |
| m.append("@@ -"); |
| appendRange(m, hunk.getCurA() + 1, hunk.getEndA() - hunk.getCurA()); |
| m.append(" +"); |
| appendRange(m, hunk.getCurB() + 1, hunk.getEndB() - hunk.getCurB()); |
| m.append(" @@"); |
| m.closeTd(); |
| |
| closeLine(m); |
| } |
| |
| private void appendRange(final SafeHtmlBuilder m, final int begin, |
| final int cnt) { |
| switch (cnt) { |
| case 0: |
| m.append(begin - 1); |
| m.append(",0"); |
| break; |
| |
| case 1: |
| m.append(begin); |
| break; |
| |
| default: |
| m.append(begin); |
| m.append(','); |
| m.append(cnt); |
| break; |
| } |
| } |
| |
| private void setStyleDelete(final SafeHtmlBuilder m, |
| boolean syntaxHighlighting) { |
| m.addStyleName(Gerrit.RESOURCES.css().diffTextDELETE()); |
| if (syntaxHighlighting) { |
| m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE()); |
| } |
| } |
| |
| private void setStyleInsert(final SafeHtmlBuilder m, |
| boolean syntaxHighlighting) { |
| m.addStyleName(Gerrit.RESOURCES.css().diffTextINSERT()); |
| if (syntaxHighlighting) { |
| m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT()); |
| } |
| } |
| |
| private void appendLineText(final SafeHtmlBuilder m, |
| boolean syntaxHighlighting, final PatchLine.Type type, |
| final SparseHtmlFile src, final int i) { |
| final SafeHtml text = src.getSafeHtmlLine(i); |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().fileLine()); |
| m.addStyleName(Gerrit.RESOURCES.css().diffText()); |
| switch (type) { |
| case CONTEXT: |
| m.addStyleName(Gerrit.RESOURCES.css().diffTextCONTEXT()); |
| m.nbsp(); |
| m.append(text); |
| break; |
| case DELETE: |
| setStyleDelete(m, syntaxHighlighting); |
| m.append("-"); |
| m.append(text); |
| break; |
| case INSERT: |
| setStyleInsert(m, syntaxHighlighting); |
| m.append("+"); |
| m.append(text); |
| break; |
| case REPLACE: |
| break; |
| } |
| m.closeTd(); |
| } |
| |
| private void appendNoLF(final SafeHtmlBuilder m) { |
| openLine(m); |
| padLineNumberForSideA(m); |
| padLineNumberForSideB(m); |
| m.openTd(); |
| m.addStyleName(Gerrit.RESOURCES.css().diffText()); |
| m.addStyleName(Gerrit.RESOURCES.css().diffTextNoLF()); |
| m.append("\\ No newline at end of file"); |
| m.closeTd(); |
| closeLine(m); |
| } |
| |
| private void openLine(final SafeHtmlBuilder m) { |
| m.openTr(); |
| m.setAttribute("valign", "top"); |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().iconCell()); |
| m.closeTd(); |
| } |
| |
| private void openTableHeaderLine(final SafeHtmlBuilder m) { |
| m.openTr(); |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().iconCell()); |
| m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); |
| m.closeTd(); |
| } |
| |
| private void closeLine(final SafeHtmlBuilder m) { |
| m.closeTr(); |
| } |
| |
| private void padLineNumberForSideB(final SafeHtmlBuilder m) { |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); |
| m.closeTd(); |
| } |
| |
| private void padLineNumberForSideA(final SafeHtmlBuilder m) { |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); |
| m.addStyleName(Gerrit.RESOURCES.css().rightBorder()); |
| m.closeTd(); |
| } |
| |
| private void appendLineNumberForSideB(final SafeHtmlBuilder m, final int idx) { |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); |
| m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>")); |
| m.closeTd(); |
| } |
| |
| private void appendLineNumberForSideA(final SafeHtmlBuilder m, final int idx) { |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); |
| m.addStyleName(Gerrit.RESOURCES.css().rightBorder()); |
| m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>")); |
| m.closeTd(); |
| } |
| |
| private void padLineNumberOnTableHeaderForSideB(final SafeHtmlBuilder m) { |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); |
| m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); |
| m.closeTd(); |
| } |
| |
| private void padLineNumberOnTableHeaderForSideA(final SafeHtmlBuilder m) { |
| m.openTd(); |
| m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); |
| m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); |
| m.addStyleName(Gerrit.RESOURCES.css().rightBorder()); |
| m.closeTd(); |
| } |
| } |