// 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.UnifiedChunkManager.LineRegionInfo; 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.JavaScriptObject; import com.google.gwt.core.client.JsArrayString; import com.google.gwt.core.client.Scheduler; 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.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.gwt.user.client.ui.InlineHTML; import com.google.gwtexpui.globalkey.client.GlobalKey; import com.google.gwtexpui.safehtml.client.SafeHtml; 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.Pos; import net.codemirror.lib.ScrollInfo; public class Unified extends DiffScreen { interface Binder extends UiBinder<FlowPanel, Unified> {} private static final Binder uiBinder = GWT.create(Binder.class); @UiField(provided = true) UnifiedTable diffTable; private CodeMirror cm; private UnifiedChunkManager chunkManager; private UnifiedCommentManager commentManager; private boolean autoHideDiffTableHeader; public Unified( DiffObject base, DiffObject revision, String path, DisplaySide startSide, int startLine) { super(base, revision, path, startSide, startLine, DiffView.UNIFIED_DIFF); diffTable = new UnifiedTable(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>(Unified.this) { @Override protected void preDisplay(ConfigInfoCache.Entry result) { commentManager = new UnifiedCommentManager( Unified.this, base, revision, path, result.getCommentLinkProcessor(), getChangeStatus().isOpen()); setTheme(result.getTheme()); display(comments); header.setupPrevNextFiles(comments); } }; } @Override public void onShowView() { super.onShowView(); operation( () -> { resizeCodeMirror(); cm.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); } else { setStartSide(d.getSide()); } setStartLine(chunkManager.getCmLine(d.getStart(), d.getSide()) + 1); } } if (getStartSide() != null && getStartLine() > 0) { cm.scrollToLine(chunkManager.getCmLine(getStartLine() - 1, getStartSide())); cm.focus(); } else { cm.setCursor(Pos.create(0)); cm.focus(); } if (Gerrit.isSignedIn() && prefs.autoReview()) { header.autoReview(); } prefetchNextFile(); } @Override void registerCmEvents(CodeMirror cm) { super.registerCmEvents(cm); cm.on( "scroll", () -> { ScrollInfo si = cm.getScrollInfo(); if (autoHideDiffTableHeader) { updateDiffTableHeader(si); } }); maybeRegisterRenderEntireFileKeyMap(cm); } @Override public void registerKeys() { super.registerKeys(); registerHandlers(); } @Override FocusHandler getFocusHandler() { return new FocusHandler() { @Override public void onFocus(FocusEvent event) { cm.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()); } cm = newCm(diff.metaA() == null ? diff.metaB() : diff.metaA(), diff.textUnified(), diffTable.cm); setShowTabs(prefs.showTabs()); chunkManager = new UnifiedChunkManager(this, cm, diffTable.scrollbar); operation( () -> { // Estimate initial CodeMirror height, fixed up in onShowView. int height = Window.getClientHeight() - (Gerrit.getHeaderFooterHeight() + 18); cm.setHeight(height); render(diff); commentManager.render(comments, prefs.expandAllComments()); skipManager.render(prefs.context(), diff); }); registerCmEvents(cm); setPrefsAction(new PreferencesAction(this, prefs)); header.init(getPrefsAction(), getSideBySideDiffLink(), diff.unifiedWebLinks()); setAutoHideDiffHeader(prefs.autoHideDiffTableHeader()); setupSyntaxHighlighting(); } private List<InlineHyperlink> getSideBySideDiffLink() { InlineHyperlink toSideBySideDiffLink = new InlineHyperlink(); toSideBySideDiffLink.setHTML( new ImageResourceRenderer().render(Gerrit.RESOURCES.sideBySideDiff())); toSideBySideDiffLink.setTargetHistoryToken(Dispatcher.toSideBySide(base, revision, path)); toSideBySideDiffLink.setTitle(PatchUtil.C.sideBySideDiff()); return Collections.singletonList(toSideBySideDiffLink); } @Override CodeMirror newCm(DiffInfo.FileMeta meta, String contents, Element parent) { JsArrayString gutters = JavaScriptObject.createArray().cast(); gutters.push(UnifiedTable.style.lineNumbersLeft()); gutters.push(UnifiedTable.style.lineNumbersRight()); return CodeMirror.create( parent, Configuration.create() .set("cursorBlinkRate", prefs.cursorBlinkRate()) .set("cursorHeight", 0.85) .set("gutters", gutters) .set("inputStyle", "textarea") .set("keyMap", "vim_ro") .set("lineNumbers", false) .set("lineWrapping", prefs.lineWrapping()) .set("matchBrackets", prefs.matchBrackets()) .set("mode", getFileSize() == FileSize.SMALL ? getContentType(meta) : null) .set("readOnly", true) .set("scrollbarStyle", "overlay") .set("styleSelectedText", true) .set("showTrailingSpace", prefs.showWhitespaceErrors()) .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); cm.refresh(); } private void setLineNumber(DisplaySide side, int cmLine, Integer line, String styleName) { SafeHtml html = SafeHtml.asis(line != null ? line.toString() : " "); InlineHTML gutter = new InlineHTML(html); diffTable.add(gutter); gutter.setStyleName(styleName); cm.setGutterMarker( cmLine, side == DisplaySide.A ? UnifiedTable.style.lineNumbersLeft() : UnifiedTable.style.lineNumbersRight(), gutter.getElement()); } void setLineNumber(DisplaySide side, int cmLine, int line) { setLineNumber(side, cmLine, line, UnifiedTable.style.unifiedLineNumber()); } void setLineNumberEmpty(DisplaySide side, int cmLine) { setLineNumber(side, cmLine, null, UnifiedTable.style.unifiedLineNumberEmpty()); } @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()) { cm.setOption( "mode", getContentType(diff.metaA() == null ? diff.metaB() : diff.metaA())); } } @Override public void onFailure(Throwable caught) { prefs.syntaxHighlighting(false); } }); } else { cm.setOption("mode", (String) null); } } @Override void setAutoHideDiffHeader(boolean autoHide) { if (autoHide) { updateDiffTableHeader(cm.getScrollInfo()); } else { diffTable.setHeaderVisible(true); } autoHideDiffTableHeader = autoHide; } private void updateDiffTableHeader(ScrollInfo si) { if (si.top() == 0) { diffTable.setHeaderVisible(true); } else if (si.top() > 0.5 * si.clientHeight()) { diffTable.setHeaderVisible(false); } } @Override Runnable updateActiveLine(CodeMirror 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( () -> { LineHandle handle = cm.getLineHandleVisualStart(cm.getCursor("end").line()); cm.extras().activeLine(handle); }); }; } @Override CodeMirror getCmFromSide(DisplaySide side) { return cm; } @Override int getCmLine(int line, DisplaySide side) { return chunkManager.getCmLine(line, side); } LineRegionInfo getLineRegionInfoFromCmLine(int cmLine) { return chunkManager.getLineRegionInfoFromCmLine(cmLine); } @Override void operation(Runnable apply) { cm.operation(apply::run); } @Override CodeMirror[] getCms() { return new CodeMirror[] {cm}; } @Override UnifiedTable getDiffTable() { return diffTable; } @Override UnifiedChunkManager getChunkManager() { return chunkManager; } @Override UnifiedCommentManager getCommentManager() { return commentManager; } @Override boolean isSideBySide() { return false; } @Override String getLineNumberClassName() { return UnifiedTable.style.unifiedLineNumber(); } }