// 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 static com.google.gerrit.client.patches.PatchLine.Type.REPLACE; 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.PatchScript.FileMode; 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.reviewdb.client.PatchLineComment; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.user.client.DOM; import com.google.gwt.user.client.Event; import com.google.gwt.user.client.ui.Anchor; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.HTMLTable.CellFormatter; import com.google.gwt.user.client.ui.HasVerticalAlignment; import com.google.gwt.user.client.ui.InlineLabel; import com.google.gwt.user.client.ui.UIObject; import com.google.gwtexpui.safehtml.client.SafeHtml; import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder; import org.eclipse.jgit.diff.Edit; import java.util.ArrayList; import java.util.Iterator; public class SideBySideTable extends AbstractPatchContentTable { private static final int A = 2; private static final int B = 3; private static final int NUM_ROWS_TO_EXPAND = 10; private SparseHtmlFile a; private SparseHtmlFile b; private boolean isHugeFile; protected boolean isFileCommentBorderRowExist; protected void createFileCommentEditorOnSideA() { createCommentEditor(R_HEAD + 1, A, R_HEAD, FILE_SIDE_A); } protected void createFileCommentEditorOnSideB() { createCommentEditor(R_HEAD + 1, B, R_HEAD, FILE_SIDE_B); } @Override protected void onCellDoubleClick(final int row, int column) { if (column > C_ARROW && getRowItem(row) instanceof PatchLine) { final PatchLine line = (PatchLine) getRowItem(row); if (column == 1 || column == A) { createCommentEditor(row + 1, A, line.getLineA(), (short) 0); } else if (column == B || column == 4) { createCommentEditor(row + 1, B, line.getLineB(), (short) 1); } } } @Override protected void onCellSingleClick(Event event, int row, int column) { super.onCellSingleClick(event, row, column); if (column == 1 || column == 4) { onCellDoubleClick(row, column); } } @Override protected void onInsertComment(final PatchLine line) { final int row = getCurrentRow(); createCommentEditor(row + 1, B, line.getLineB(), (short) 1); } @Override protected void render(final PatchScript script, final PatchSetDetail detail) { final ArrayList<Object> lines = new ArrayList<>(); final SafeHtmlBuilder nc = new SafeHtmlBuilder(); isHugeFile = script.isHugeFile(); allocateTableHeader(script, nc); lines.add(null); if (!isDisplayBinary) { if (script.getFileModeA() != FileMode.FILE || script.getFileModeB() != FileMode.FILE) { openLine(nc); appendModeLine(nc, script.getFileModeA()); appendModeLine(nc, script.getFileModeB()); closeLine(nc); lines.add(null); } if (hasDifferences(script)) { int lastA = 0; int lastB = 0; final boolean ignoreWS = script.isIgnoreWhitespace(); a = getSparseHtmlFileA(script); b = getSparseHtmlFileB(script); final boolean intraline = script.getDiffPrefs().isIntralineDifference() && script.hasIntralineDifference(); for (final EditList.Hunk hunk : script.getHunks()) { if (!hunk.isStartOfFile()) { appendSkipLine(nc, hunk.getCurB() - lastB); lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB)); } while (hunk.next()) { if (hunk.isContextLine()) { openLine(nc); final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA()); appendLineNumber(nc, hunk.getCurA(), false); appendLineText(nc, CONTEXT, ctx, false, false); if (ignoreWS && b.contains(hunk.getCurB())) { appendLineText(nc, CONTEXT, b, hunk.getCurB(), false); } else { appendLineText(nc, CONTEXT, ctx, false, false); } appendLineNumber(nc, hunk.getCurB(), true); closeLine(nc); hunk.incBoth(); lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB())); } else if (hunk.isModifiedLine()) { final boolean del = hunk.isDeletedA(); final boolean ins = hunk.isInsertedB(); final boolean full = intraline && hunk.getCurEdit().getType() != Edit.Type.REPLACE; openLine(nc); if (del) { appendLineNumber(nc, hunk.getCurA(), false); appendLineText(nc, DELETE, a, hunk.getCurA(), full); hunk.incA(); } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) { appendLineNumber(nc, false); appendLineNone(nc, DELETE); } else { appendLineNumber(nc, false); appendLineNone(nc, CONTEXT); } if (ins) { appendLineText(nc, INSERT, b, hunk.getCurB(), full); appendLineNumber(nc, hunk.getCurB(), true); hunk.incB(); } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) { appendLineNone(nc, INSERT); appendLineNumber(nc, true); } else { appendLineNone(nc, CONTEXT); appendLineNumber(nc, true); } closeLine(nc); if (del && ins) { lines.add(new PatchLine(REPLACE, hunk.getCurA(), hunk.getCurB())); } else if (del) { lines.add(new PatchLine(DELETE, hunk.getCurA(), -1)); } else if (ins) { lines.add(new PatchLine(INSERT, -1, hunk.getCurB())); } } } lastA = hunk.getCurA(); lastB = hunk.getCurB(); } if (lastB != b.size()) { appendSkipLine(nc, b.size() - lastB); lines.add(new SkippedLine(lastA, lastB, b.size() - lastB)); } } } else { // Display the patch header for binary for (final String line : script.getPatchHeader()) { appendFileHeader(nc, line); } // If there is a safe picture involved, we show it if (script.getDisplayMethodA() == DisplayMethod.IMG || script.getDisplayMethodB() == DisplayMethod.IMG) { appendImageLine(script, nc); } } if (!hasDifferences(script)) { appendNoDifferences(nc); } resetHtml(nc); populateTableHeader(script, detail); if (hasDifferences(script)) { initScript(script); if (!isDisplayBinary) { for (int row = 0; row < lines.size(); row++) { setRowItem(row, lines.get(row)); if (lines.get(row) instanceof SkippedLine) { createSkipLine(row, (SkippedLine) lines.get(row), isHugeFile); } } } } } private SafeHtml createImage(String url) { SafeHtmlBuilder m = new SafeHtmlBuilder(); m.openElement("img"); m.setAttribute("src", url); m.closeElement("img"); return m.toSafeHtml(); } private void appendImageLine(final PatchScript script, final SafeHtmlBuilder m) { m.openTr(); m.setAttribute("valign", "center"); m.setAttribute("align", "center"); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().iconCell()); m.closeTd(); appendLineNumber(m, false); if (script.getDisplayMethodA() == DisplayMethod.IMG) { final String url = getUrlA(); appendLineText(m, DELETE, createImage(url), false, true); } else { appendLineNone(m, DELETE); } if (script.getDisplayMethodB() == DisplayMethod.IMG) { final String url = getUrlB(); appendLineText(m, INSERT, createImage(url), false, true); } else { appendLineNone(m, INSERT); } appendLineNumber(m, true); m.closeTr(); } private void populateTableHeader(final PatchScript script, final PatchSetDetail detail) { initHeaders(script, detail); table.setWidget(R_HEAD, A, headerSideA); table.setWidget(R_HEAD, B, headerSideB); // Populate icons to lineNumber column header. if (headerSideA.isFileOrCommitMessage()) { table.setWidget(R_HEAD, A - 1, iconA); } if (headerSideB.isFileOrCommitMessage()) { table.setWidget(R_HEAD, B + 1, iconB); } } private void appendModeLine(final SafeHtmlBuilder nc, final FileMode mode) { nc.openTd(); nc.setStyleName(Gerrit.RESOURCES.css().lineNumber()); nc.nbsp(); nc.closeTd(); nc.openTd(); nc.addStyleName(Gerrit.RESOURCES.css().fileLine()); nc.addStyleName(Gerrit.RESOURCES.css().fileLineMode()); switch(mode){ case FILE: nc.nbsp(); break; case SYMLINK: nc.append(PatchUtil.C.fileTypeSymlink()); break; case GITLINK: nc.append(PatchUtil.C.fileTypeGitlink()); break; } nc.closeTd(); } @Override protected PatchScreen.Type getPatchScreenType() { return PatchScreen.Type.SIDE_BY_SIDE; } @Override public void display(final CommentDetail cd, boolean expandComments) { if (cd.isEmpty()) { return; } setAccountInfoCache(cd.getAccounts()); for (int row = 0; row < table.getRowCount();) { final Iterator<PatchLineComment> ai; final Iterator<PatchLineComment> bi; if (row == R_HEAD) { ai = cd.getForA(R_HEAD).iterator(); bi = cd.getForB(R_HEAD).iterator(); } else if (getRowItem(row) instanceof PatchLine) { final PatchLine pLine = (PatchLine) getRowItem(row); ai = cd.getForA(pLine.getLineA()).iterator(); bi = cd.getForB(pLine.getLineB()).iterator(); } else { row++; continue; } row++; while (ai.hasNext() && bi.hasNext()) { final PatchLineComment ac = ai.next(); final PatchLineComment bc = bi.next(); if (ac.getLine() == R_HEAD) { insertFileCommentRow(row); } else { insertRow(row); } bindComment(row, A, ac, !ai.hasNext(), expandComments); bindComment(row, B, bc, !bi.hasNext(), expandComments); row++; } row = finish(ai, row, A, expandComments); row = finish(bi, row, B, expandComments); } } private void defaultStyle(final int row, final CellFormatter fmt) { fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber()); fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText()); if (isDisplayBinary) { fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffTextForBinaryInSideBySide()); } fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText()); fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber()); fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().rightmost()); } @Override protected void insertRow(final int row) { super.insertRow(row); final CellFormatter fmt = table.getCellFormatter(); defaultStyle(row, fmt); } @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, A - 1, // Gerrit.RESOURCES.css().cellsNextToFileComment()); fmt.addStyleName(row, B + 1, // Gerrit.RESOURCES.css().cellsNextToFileComment()); createFileCommentBorderRow(row); } private void createFileCommentBorderRow(final int row) { if (row == 1 && !isFileCommentBorderRowExist) { isFileCommentBorderRowExist = true; table.insertRow(R_HEAD + 2); final CellFormatter fmt = table.getCellFormatter(); fmt.addStyleName(R_HEAD + 2, C_ARROW, // Gerrit.RESOURCES.css().iconCellOfFileCommentRow()); defaultStyle(R_HEAD + 2, fmt); final Element iconCell = fmt.getElement(R_HEAD + 2, C_ARROW); UIObject.setStyleName(DOM.getParent(iconCell), Gerrit.RESOURCES.css() .fileCommentBorder(), true); } } private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) { while (i.hasNext()) { final PatchLineComment c = i.next(); if (c.getLine() == R_HEAD) { insertFileCommentRow(row); } else { insertRow(row); } bindComment(row, col, c, !i.hasNext(), expandComment); row++; } return row; } private void allocateTableHeader(PatchScript script, final SafeHtmlBuilder m) { m.openTr(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().iconCell()); m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); m.closeTd(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); m.nbsp(); m.closeTd(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); m.addStyleName(Gerrit.RESOURCES.css().fileLine()); m.closeTd(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); m.addStyleName(Gerrit.RESOURCES.css().fileLine()); m.closeTd(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader()); m.addStyleName(Gerrit.RESOURCES.css().rightmost()); m.closeTd(); m.closeTr(); } private void appendFileHeader(final SafeHtmlBuilder m, final String line) { m.openTr(); m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().iconCell()); m.closeTd(); appendLineNumber(m, false); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().sideBySideTableBinaryHeader()); m.setAttribute("colspan", 2); m.append(line); m.closeTd(); appendLineNumber(m, true); m.closeTr(); } private void appendSkipLine(final SafeHtmlBuilder m, final int skipCnt) { m.openTr(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().iconCell()); m.addStyleName(Gerrit.RESOURCES.css().skipLine()); m.closeTd(); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().skipLine()); m.setAttribute("colspan", 4); m.closeTd(); m.closeTr(); } private ClickHandler expandAllListener = new ClickHandler() { @Override public void onClick(ClickEvent event) { expand(event, 0); } }; private ClickHandler expandBeforeListener = new ClickHandler() { @Override public void onClick(ClickEvent event) { expand(event, NUM_ROWS_TO_EXPAND); } }; private ClickHandler expandAfterListener = new ClickHandler() { @Override public void onClick(ClickEvent event) { expand(event, -NUM_ROWS_TO_EXPAND); } }; private void expand(ClickEvent event, final int numRows) { int row = table.getCellForEvent(event).getRowIndex(); if (!(getRowItem(row) instanceof SkippedLine)) { return; } SkippedLine line = (SkippedLine) getRowItem(row); int loopTo = numRows; if (numRows == 0) { loopTo = line.getSize(); } else if (numRows < 0) { loopTo = -numRows; } int offset = 0; if (numRows < 0) { offset = 1; } CellFormatter fmt = table.getCellFormatter(); for (int i = 0 + offset; i < loopTo + offset; i++) { insertRow(row + i); table.getRowFormatter().setVerticalAlign(row + i, HasVerticalAlignment.ALIGN_TOP); int lineA = line.getStartA() + i; int lineB = line.getStartB() + i; if (numRows < 0) { lineA = line.getStartA() + line.getSize() + numRows + i - offset; lineB = line.getStartB() + line.getSize() + numRows + i - offset; } table.setHTML(row + i, A - 1, "<a href=\"javascript:;\">" + (lineA + 1) + "</a>"); fmt.addStyleName(row + i, A - 1, Gerrit.RESOURCES.css().lineNumber()); table.setHTML(row + i, A, a.getSafeHtmlLine(lineA).asString()); fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLine()); fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLineCONTEXT()); table.setHTML(row + i, B, b.getSafeHtmlLine(lineB).asString()); fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLine()); fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLineCONTEXT()); table.setHTML(row + i, B + 1, "<a href=\"javascript:;\">" + (lineB + 1) + "</a>"); fmt.addStyleName(row + i, B + 1, Gerrit.RESOURCES.css().lineNumber()); setRowItem(row + i, new PatchLine(CONTEXT, lineA, lineB)); } if (numRows > 0) { line.incrementStart(numRows); createSkipLine(row + loopTo, line, isHugeFile); } else if (numRows < 0) { line.reduceSize(-numRows); createSkipLine(row, line, isHugeFile); } else { table.removeRow(row + loopTo); } } private void createSkipLine(int row, SkippedLine line, boolean isHugeFile) { FlowPanel p = new FlowPanel(); InlineLabel l1 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionStart() + " "); InlineLabel l2 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionEnd() + " "); Anchor all = new Anchor(String.valueOf(line.getSize())); all.addClickHandler(expandAllListener); all.setStyleName(Gerrit.RESOURCES.css().skipLine()); if (line.getSize() > 30) { // Only show the expand before/after if skipped more than 30 lines. Anchor b = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND), true); Anchor a = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND), true); b.addClickHandler(expandBeforeListener); a.addClickHandler(expandAfterListener); b.setStyleName(Gerrit.RESOURCES.css().skipLine()); a.setStyleName(Gerrit.RESOURCES.css().skipLine()); p.add(b); p.add(l1); if (isHugeFile) { p.add(new InlineLabel(" " + line.getSize() + " ")); } else { p.add(all); } p.add(l2); p.add(a); } else { p.add(l1); p.add(all); p.add(l2); } table.setWidget(row, 1, p); } private void openLine(final SafeHtmlBuilder m) { m.openTr(); m.setAttribute("valign", "top"); m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().iconCell()); m.closeTd(); } private void appendLineNumber(SafeHtmlBuilder m, boolean right) { m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); if (right) { m.addStyleName(Gerrit.RESOURCES.css().rightmost()); } m.closeTd(); } private void appendLineNumber(SafeHtmlBuilder m, int lineNumberMinusOne, boolean right) { m.openTd(); m.setStyleName(Gerrit.RESOURCES.css().lineNumber()); if (right) { m.addStyleName(Gerrit.RESOURCES.css().rightmost()); } m.append(SafeHtml.asis("<a href=\"javascript:;\">"+ (lineNumberMinusOne + 1) + "</a>")); m.closeTd(); } private void appendLineText(final SafeHtmlBuilder m, final PatchLine.Type type, final SparseHtmlFile src, final int i, final boolean fullBlock) { appendLineText(m, type, src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock); } private void appendLineText(final SafeHtmlBuilder m, final PatchLine.Type type, final SafeHtml lineHtml, final boolean trailingEdit, final boolean fullBlock) { m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().fileLine()); switch (type) { case CONTEXT: m.addStyleName(Gerrit.RESOURCES.css().fileLineCONTEXT()); break; case DELETE: m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE()); if (trailingEdit || fullBlock) { m.addStyleName("wdd"); } break; case INSERT: m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT()); if (trailingEdit || fullBlock) { m.addStyleName("wdi"); } break; case REPLACE: break; } m.append(lineHtml); m.closeTd(); } private void appendLineNone(final SafeHtmlBuilder m, final PatchLine.Type type) { m.openTd(); m.addStyleName(Gerrit.RESOURCES.css().fileLine()); switch (type != null ? type : PatchLine.Type.CONTEXT) { case DELETE: m.addStyleName(Gerrit.RESOURCES.css().fileLineDELETE()); break; case INSERT: m.addStyleName(Gerrit.RESOURCES.css().fileLineINSERT()); break; default: m.addStyleName(Gerrit.RESOURCES.css().fileLineNone()); break; } m.closeTd(); } private void closeLine(final SafeHtmlBuilder m) { m.closeTr(); } @Override protected void destroyCommentRow(final int row) { super.destroyCommentRow(row); if (row == R_HEAD + 1) { table.removeRow(row); isFileCommentBorderRowExist = false; } } }