// 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>() { 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"); } protected void createFileCommentEditorOnSideA() { createCommentEditor(R_HEAD + 1, PC, R_HEAD, FILE_SIDE_A); } 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.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, expandComments); } rowOfTableHeaderB = row; borderRowOfFileComment = row + 1; if (!forb.isEmpty()) { row++;// Skip the Header of sideB. row = insert(forb, row, expandComments); 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, expandComments); } else if (!fora.isEmpty()) { row = insert(fora, row, expandComments); } else if (!forb.isEmpty()) { row = insert(forb, row, expandComments); } } 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); } @Override protected PatchScreen.Type getPatchScreenType() { return PatchScreen.Type.UNIFIED; } private int insert(final List<PatchLineComment> in, int row, boolean expandComment) { 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(), expandComment); 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(); } }