| // 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.common.Nullable; |
| import com.google.gerrit.extensions.client.GeneralPreferencesInfo.DiffView; |
| import com.google.gerrit.reviewdb.client.Patch; |
| import com.google.gerrit.reviewdb.client.Project; |
| 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( |
| @Nullable Project.NameKey project, |
| DiffObject base, |
| DiffObject revision, |
| String path, |
| DisplaySide startSide, |
| int startLine) { |
| super(project, 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, |
| getProject(), |
| 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(getProject(), 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(); |
| } |
| } |