// Copyright (C) 2013 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.diff; import static java.lang.Double.POSITIVE_INFINITY; import com.google.gerrit.client.DiffObject; import com.google.gerrit.client.Dispatcher; import com.google.gerrit.client.Gerrit; import com.google.gerrit.client.diff.LineMapper.LineOnOtherInfo; import com.google.gerrit.client.patches.PatchUtil; import com.google.gerrit.client.projects.ConfigInfoCache; import com.google.gerrit.client.rpc.ScreenLoadCallback; import com.google.gerrit.client.ui.InlineHyperlink; import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView; import com.google.gerrit.reviewdb.client.Patch; import com.google.gwt.core.client.GWT; import com.google.gwt.core.client.Scheduler; import com.google.gwt.core.client.Scheduler.ScheduledCommand; import com.google.gwt.dom.client.Element; import com.google.gwt.event.dom.client.FocusEvent; import com.google.gwt.event.dom.client.FocusHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.event.dom.client.KeyPressEvent; import com.google.gwt.uibinder.client.UiBinder; import com.google.gwt.uibinder.client.UiField; import com.google.gwt.user.client.Window; import com.google.gwt.user.client.rpc.AsyncCallback; import com.google.gwt.user.client.ui.FlowPanel; import com.google.gwt.user.client.ui.ImageResourceRenderer; import com.google.gwtexpui.globalkey.client.GlobalKey; import com.google.gwtexpui.globalkey.client.KeyCommand; import java.util.Collections; import java.util.List; import net.codemirror.lib.CodeMirror; import net.codemirror.lib.CodeMirror.LineHandle; import net.codemirror.lib.Configuration; import net.codemirror.lib.KeyMap; import net.codemirror.lib.Pos; public class SideBySide extends DiffScreen { interface Binder extends UiBinder<FlowPanel, SideBySide> {} private static final Binder uiBinder = GWT.create(Binder.class); private static final String LINE_NUMBER_CLASSNAME = "CodeMirror-linenumber"; @UiField(provided = true) SideBySideTable diffTable; private CodeMirror cmA; private CodeMirror cmB; private ScrollSynchronizer scrollSynchronizer; private SideBySideChunkManager chunkManager; private SideBySideCommentManager commentManager; public SideBySide( DiffObject base, DiffObject revision, String path, DisplaySide startSide, int startLine) { super(base, revision, path, startSide, startLine, DiffView.SIDE_BY_SIDE); diffTable = new SideBySideTable(this, base, revision, path); add(uiBinder.createAndBindUi(this)); addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType()); } @Override ScreenLoadCallback<ConfigInfoCache.Entry> getScreenLoadCallback( final CommentsCollections comments) { return new ScreenLoadCallback<ConfigInfoCache.Entry>(SideBySide.this) { @Override protected void preDisplay(ConfigInfoCache.Entry result) { commentManager = new SideBySideCommentManager( SideBySide.this, base, revision, path, result.getCommentLinkProcessor(), getChangeStatus().isOpen()); setTheme(result.getTheme()); display(comments); header.setupPrevNextFiles(comments); } }; } @Override public void onShowView() { super.onShowView(); operation( () -> { resizeCodeMirror(); chunkManager.adjustPadding(); cmA.refresh(); cmB.refresh(); }); setLineLength(Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength()); diffTable.refresh(); if (getStartLine() == 0) { DiffChunkInfo d = chunkManager.getFirst(); if (d != null) { if (d.isEdit() && d.getSide() == DisplaySide.A) { setStartSide(DisplaySide.B); setStartLine(lineOnOther(d.getSide(), d.getStart()).getLine() + 1); } else { setStartSide(d.getSide()); setStartLine(d.getStart() + 1); } } } if (getStartSide() != null && getStartLine() > 0) { CodeMirror cm = getCmFromSide(getStartSide()); cm.scrollToLine(getStartLine() - 1); cm.focus(); } else { cmA.setCursor(Pos.create(0)); cmA.focus(); } if (Gerrit.isSignedIn() && prefs.autoReview()) { header.autoReview(); } prefetchNextFile(); } @Override void registerCmEvents(final CodeMirror cm) { super.registerCmEvents(cm); KeyMap keyMap = KeyMap.create() .on("Shift-A", diffTable.toggleA()) .on("Shift-Left", moveCursorToSide(cm, DisplaySide.A)) .on("Shift-Right", moveCursorToSide(cm, DisplaySide.B)); cm.addKeyMap(keyMap); maybeRegisterRenderEntireFileKeyMap(cm); } @Override public void registerKeys() { super.registerKeys(); getKeysNavigation() .add( new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_LEFT, PatchUtil.C.focusSideA()), new NoOpKeyCommand(KeyCommand.M_SHIFT, KeyCodes.KEY_RIGHT, PatchUtil.C.focusSideB())); getKeysAction() .add( new KeyCommand(KeyCommand.M_SHIFT, 'a', PatchUtil.C.toggleSideA()) { @Override public void onKeyPress(KeyPressEvent event) { diffTable.toggleA().run(); } }); registerHandlers(); } @Override FocusHandler getFocusHandler() { return new FocusHandler() { @Override public void onFocus(FocusEvent event) { cmB.focus(); } }; } private void display(CommentsCollections comments) { DiffInfo diff = getDiff(); setThemeStyles(prefs.theme().isDark()); setShowIntraline(prefs.intralineDifference()); if (prefs.showLineNumbers()) { diffTable.addStyleName(Resources.I.diffTableStyle().showLineNumbers()); } cmA = newCm(diff.metaA(), diff.textA(), diffTable.cmA); cmB = newCm(diff.metaB(), diff.textB(), diffTable.cmB); getDiffTable() .setUpBlameIconA( cmA, base.isBaseOrAutoMerge(), base.isBaseOrAutoMerge() ? revision : base.asPatchSetId(), path); getDiffTable().setUpBlameIconB(cmB, revision, path); cmA.extras().side(DisplaySide.A); cmB.extras().side(DisplaySide.B); setShowTabs(prefs.showTabs()); chunkManager = new SideBySideChunkManager(this, cmA, cmB, diffTable.scrollbar); operation( () -> { // Estimate initial CodeMirror height, fixed up in onShowView. int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18); cmA.setHeight(height); cmB.setHeight(height); render(diff); commentManager.render(comments, prefs.expandAllComments()); skipManager.render(prefs.context(), diff); }); registerCmEvents(cmA); registerCmEvents(cmB); scrollSynchronizer = new ScrollSynchronizer(diffTable, cmA, cmB, chunkManager.lineMapper); setPrefsAction(new PreferencesAction(this, prefs)); header.init(getPrefsAction(), getUnifiedDiffLink(), diff.sideBySideWebLinks()); scrollSynchronizer.setAutoHideDiffTableHeader(prefs.autoHideDiffTableHeader()); setupSyntaxHighlighting(); } private List<InlineHyperlink> getUnifiedDiffLink() { InlineHyperlink toUnifiedDiffLink = new InlineHyperlink(); toUnifiedDiffLink.setHTML(new ImageResourceRenderer().render(Gerrit.RESOURCES.unifiedDiff())); toUnifiedDiffLink.setTargetHistoryToken(Dispatcher.toUnified(base, revision, path)); toUnifiedDiffLink.setTitle(PatchUtil.C.unifiedDiff()); return Collections.singletonList(toUnifiedDiffLink); } @Override CodeMirror newCm(DiffInfo.FileMeta meta, String contents, Element parent) { return CodeMirror.create( parent, Configuration.create() .set("cursorBlinkRate", prefs.cursorBlinkRate()) .set("cursorHeight", 0.85) .set("inputStyle", "textarea") .set("keyMap", "vim_ro") .set("lineNumbers", prefs.showLineNumbers()) .set("matchBrackets", prefs.matchBrackets()) .set("lineWrapping", prefs.lineWrapping()) .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null) .set("readOnly", true) .set("scrollbarStyle", "overlay") .set("showTrailingSpace", prefs.showWhitespaceErrors()) .set("styleSelectedText", true) .set("tabSize", prefs.tabSize()) .set("theme", prefs.theme().name().toLowerCase()) .set("value", meta != null ? contents : "") .set("viewportMargin", renderEntireFile() ? POSITIVE_INFINITY : 10)); } @Override void setShowLineNumbers(boolean b) { super.setShowLineNumbers(b); cmA.setOption("lineNumbers", b); cmB.setOption("lineNumbers", b); } @Override void setSyntaxHighlighting(boolean b) { final DiffInfo diff = getDiff(); if (b) { injectMode( diff, new AsyncCallback<Void>() { @Override public void onSuccess(Void result) { if (prefs.syntaxHighlighting()) { cmA.setOption("mode", getContentType(diff.metaA())); cmB.setOption("mode", getContentType(diff.metaB())); } } @Override public void onFailure(Throwable caught) { prefs.syntaxHighlighting(false); } }); } else { cmA.setOption("mode", (String) null); cmB.setOption("mode", (String) null); } } @Override void setAutoHideDiffHeader(boolean hide) { scrollSynchronizer.setAutoHideDiffTableHeader(hide); } CodeMirror otherCm(CodeMirror me) { return me == cmA ? cmB : cmA; } @Override CodeMirror getCmFromSide(DisplaySide side) { return side == DisplaySide.A ? cmA : cmB; } @Override int getCmLine(int line, DisplaySide side) { return line; } @Override Runnable updateActiveLine(CodeMirror cm) { CodeMirror other = otherCm(cm); return () -> { // The rendering of active lines has to be deferred. Reflow // caused by adding and removing styles chokes Firefox when arrow // key (or j/k) is held down. Performance on Chrome is fine // without the deferral. // Scheduler.get() .scheduleDeferred( new ScheduledCommand() { @Override public void execute() { operation( () -> { LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line()); if (!cm.extras().activeLine(handle)) { return; } LineOnOtherInfo info = lineOnOther(cm.side(), cm.getLineNumber(handle)); if (info.isAligned()) { other.extras().activeLine(other.getLineHandle(info.getLine())); } else { other.extras().clearActiveLine(); } }); } }); }; } private Runnable moveCursorToSide(CodeMirror cmSrc, DisplaySide sideDst) { CodeMirror cmDst = getCmFromSide(sideDst); if (cmDst == cmSrc) { return () -> {}; } DisplaySide sideSrc = cmSrc.side(); return () -> { if (cmSrc.extras().hasActiveLine()) { cmDst.setCursor( Pos.create( lineOnOther(sideSrc, cmSrc.getLineNumber(cmSrc.extras().activeLine())).getLine())); } cmDst.focus(); }; } void syncScroll(DisplaySide masterSide) { if (scrollSynchronizer != null) { scrollSynchronizer.syncScroll(masterSide); } } @Override void operation(Runnable apply) { cmA.operation(() -> cmB.operation(apply::run)); } @Override CodeMirror[] getCms() { return new CodeMirror[] {cmA, cmB}; } @Override SideBySideTable getDiffTable() { return diffTable; } @Override SideBySideChunkManager getChunkManager() { return chunkManager; } @Override SideBySideCommentManager getCommentManager() { return commentManager; } @Override boolean isSideBySide() { return true; } @Override String getLineNumberClassName() { return LINE_NUMBER_CLASSNAME; } }