blob: efdbe44b803aedaca15b0b5f5acc849290fb4a33 [file] [log] [blame]
// Copyright (C) 2014 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.editor;
import static com.google.gwt.dom.client.Style.Visibility.HIDDEN;
import static com.google.gwt.dom.client.Style.Visibility.VISIBLE;
import com.google.gerrit.client.DiffWebLinkInfo;
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.JumpKeys;
import com.google.gerrit.client.VoidResult;
import com.google.gerrit.client.account.DiffPreferences;
import com.google.gerrit.client.changes.ChangeApi;
import com.google.gerrit.client.changes.ChangeEditApi;
import com.google.gerrit.client.changes.ChangeInfo;
import com.google.gerrit.client.diff.DiffApi;
import com.google.gerrit.client.diff.DiffInfo;
import com.google.gerrit.client.diff.FileInfo;
import com.google.gerrit.client.diff.Header;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.HttpCallback;
import com.google.gerrit.client.rpc.HttpResponse;
import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.rpc.RestApi;
import com.google.gerrit.client.rpc.ScreenLoadCallback;
import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.Screen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.JsArray;
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.ClickEvent;
import com.google.gwt.event.dom.client.KeyPressEvent;
import com.google.gwt.event.logical.shared.ResizeEvent;
import com.google.gwt.event.logical.shared.ResizeHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.uibinder.client.UiBinder;
import com.google.gwt.uibinder.client.UiField;
import com.google.gwt.uibinder.client.UiHandler;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.Window.ClosingEvent;
import com.google.gwt.user.client.Window.ClosingHandler;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.ImageResourceRenderer;
import com.google.gwtexpui.globalkey.client.GlobalKey;
import com.google.gwtexpui.safehtml.client.SafeHtml;
import net.codemirror.lib.CodeMirror;
import net.codemirror.lib.CodeMirror.ChangesHandler;
import net.codemirror.lib.Configuration;
import net.codemirror.lib.KeyMap;
import net.codemirror.lib.Pos;
import net.codemirror.mode.ModeInfo;
import net.codemirror.mode.ModeInjector;
import net.codemirror.theme.ThemeLoader;
import java.util.List;
public class EditScreen extends Screen {
interface Binder extends UiBinder<HTMLPanel, EditScreen> {}
private static final Binder uiBinder = GWT.create(Binder.class);
private final PatchSet.Id base;
private final PatchSet.Id revision;
private final String path;
private final int startLine;
private DiffPreferences prefs;
private CodeMirror cm;
private HttpResponse<NativeString> content;
private EditFileInfo editFileInfo;
private JsArray<DiffWebLinkInfo> diffLinks;
@UiField Element header;
@UiField Element project;
@UiField Element filePath;
@UiField FlowPanel linkPanel;
@UiField Element cursLine;
@UiField Element cursCol;
@UiField Element dirty;
@UiField Button close;
@UiField Button save;
@UiField Element editor;
private HandlerRegistration resizeHandler;
private HandlerRegistration closeHandler;
private int generation;
public EditScreen(PatchSet.Id base, Patch.Key patch, int startLine) {
this.base = base;
this.revision = patch.getParentKey();
this.path = patch.get();
this.startLine = startLine - 1;
prefs = DiffPreferences.create(Gerrit.getAccountDiffPreference());
add(uiBinder.createAndBindUi(this));
addDomHandler(GlobalKey.STOP_PROPAGATION, KeyPressEvent.getType());
}
@Override
protected void onInitUI() {
super.onInitUI();
setHeaderVisible(false);
setWindowTitle(FileInfo.getFileName(path));
}
@Override
protected void onLoad() {
super.onLoad();
CallbackGroup group1 = new CallbackGroup();
final CallbackGroup group2 = new CallbackGroup();
final CallbackGroup group3 = new CallbackGroup();
CodeMirror.initLibrary(group1.add(new AsyncCallback<Void>() {
final AsyncCallback<Void> themeCallback = group3.addEmpty();
@Override
public void onSuccess(Void result) {
// Load theme after CM library to ensure theme can override CSS.
ThemeLoader.loadTheme(prefs.theme(), themeCallback);
group2.done();
group3.done();
}
@Override
public void onFailure(Throwable caught) {
}
}));
ChangeApi.detail(revision.getParentKey().get(),
group1.add(new AsyncCallback<ChangeInfo>() {
@Override
public void onSuccess(ChangeInfo c) {
project.setInnerText(c.project());
SafeHtml.setInnerHTML(filePath, Header.formatPath(path, null, null));
}
@Override
public void onFailure(Throwable caught) {
}
}));
if (revision.get() == 0) {
ChangeEditApi.getMeta(revision, path,
group1.add(new AsyncCallback<EditFileInfo>() {
@Override
public void onSuccess(EditFileInfo editInfo) {
editFileInfo = editInfo;
}
@Override
public void onFailure(Throwable e) {
}
}));
} else {
// TODO(davido): We probably want to create dedicated GET EditScreenMeta
// REST endpoint. Abuse GET diff for now, as it retrieves links we need.
DiffApi.diff(revision, path)
.base(base)
.webLinksOnly()
.get(group1.add(new AsyncCallback<DiffInfo>() {
@Override
public void onSuccess(DiffInfo diffInfo) {
diffLinks = diffInfo.web_links();
}
@Override
public void onFailure(Throwable e) {
}
}));
}
ChangeEditApi.get(revision, path,
group2.add(new HttpCallback<NativeString>() {
final AsyncCallback<Void> modeCallback = group3.addEmpty();
@Override
public void onSuccess(HttpResponse<NativeString> fc) {
content = fc;
if (prefs.syntaxHighlighting()) {
injectMode(fc.getContentType(), modeCallback);
} else {
modeCallback.onSuccess(null);
}
}
@Override
public void onFailure(Throwable e) {
// "Not Found" means it's a new file.
if (RestApi.isNotFound(e)) {
content = null;
modeCallback.onSuccess(null);
} else {
GerritCallback.showFailure(e);
}
}
}));
group3.addListener(new ScreenLoadCallback<Void>(this) {
@Override
protected void preDisplay(Void result) {
initEditor(content);
content = null;
renderLinks(editFileInfo, diffLinks);
editFileInfo = null;
diffLinks = null;
}
});
group1.done();
}
@Override
public void registerKeys() {
super.registerKeys();
cm.addKeyMap(KeyMap.create()
.on("Ctrl-L", gotoLine())
.on("Cmd-L", gotoLine()));
}
private Runnable gotoLine() {
return new Runnable() {
@Override
public void run() {
String n = Window.prompt(EditConstants.I.gotoLineNumber(), "");
if (n != null) {
try {
int line = Integer.parseInt(n);
line--;
if (line >= 0) {
cm.scrollToLine(line);
}
} catch (NumberFormatException e) {
// ignore non valid numbers
// We don't want to popup another ugly dialog just to say
// "The number you've provided is invalid, try again"
}
}
}
};
}
@Override
public void onShowView() {
super.onShowView();
Window.enableScrolling(false);
JumpKeys.enable(false);
if (prefs.hideTopMenu()) {
Gerrit.setHeaderVisible(false);
}
resizeHandler = Window.addResizeHandler(new ResizeHandler() {
@Override
public void onResize(ResizeEvent event) {
cm.adjustHeight(header.getOffsetHeight());
}
});
closeHandler = Window.addWindowClosingHandler(new ClosingHandler() {
@Override
public void onWindowClosing(ClosingEvent event) {
if (!cm.isClean(generation)) {
event.setMessage(EditConstants.I.closeUnsavedChanges());
}
}
});
generation = cm.changeGeneration(true);
setClean(true);
cm.on(new ChangesHandler() {
@Override
public void handle(CodeMirror cm) {
setClean(cm.isClean(generation));
}
});
cm.adjustHeight(header.getOffsetHeight());
cm.on("cursorActivity", updateCursorPosition());
cm.extras().showTabs(prefs.showTabs());
cm.extras().lineLength(
Patch.COMMIT_MSG.equals(path) ? 72 : prefs.lineLength());
cm.refresh();
cm.focus();
if (startLine > 0) {
cm.scrollToLine(startLine);
}
updateActiveLine();
}
@Override
protected void onUnload() {
super.onUnload();
if (cm != null) {
cm.getWrapperElement().removeFromParent();
}
if (resizeHandler != null) {
resizeHandler.removeHandler();
}
if (closeHandler != null) {
closeHandler.removeHandler();
}
Window.enableScrolling(true);
Gerrit.setHeaderVisible(true);
JumpKeys.enable(true);
}
@UiHandler("save")
void onSave(@SuppressWarnings("unused") ClickEvent e) {
save().run();
}
@UiHandler("close")
void onClose(@SuppressWarnings("unused") ClickEvent e) {
if (cm.isClean(generation)
|| Window.confirm(EditConstants.I.cancelUnsavedChanges())) {
upToChange();
}
}
private void upToChange() {
Gerrit.display(PageLinks.toChangeInEditMode(revision.getParentKey()));
}
private void initEditor(HttpResponse<NativeString> file) {
ModeInfo mode = null;
String content = "";
if (file != null && file.getResult() != null) {
content = file.getResult().asString();
if (prefs.syntaxHighlighting()) {
mode = ModeInfo.findMode(file.getContentType(), path);
}
}
cm = CodeMirror.create(editor, Configuration.create()
.set("value", content)
.set("readOnly", false)
.set("cursorBlinkRate", 0)
.set("cursorHeight", 0.85)
.set("lineNumbers", true)
.set("tabSize", prefs.tabSize())
.set("lineWrapping", false)
.set("scrollbarStyle", "overlay")
.set("styleSelectedText", true)
.set("showTrailingSpace", true)
.set("keyMap", "default")
.set("theme", prefs.theme().name().toLowerCase())
.set("mode", mode != null ? mode.mode() : null));
cm.addKeyMap(KeyMap.create()
.on("Cmd-S", save())
.on("Ctrl-S", save()));
}
private void renderLinks(EditFileInfo editInfo,
JsArray<DiffWebLinkInfo> diffLinks) {
renderLinksToDiff();
if (editInfo != null) {
renderLinks(Natives.asList(editInfo.web_links()));
} else if (diffLinks != null) {
renderLinks(Natives.asList(diffLinks));
}
}
private void renderLinks(List<DiffWebLinkInfo> links) {
if (links != null) {
for (DiffWebLinkInfo webLink : links) {
linkPanel.add(webLink.toAnchor());
}
}
}
private void renderLinksToDiff() {
InlineHyperlink sbs = new InlineHyperlink();
sbs.setHTML(new ImageResourceRenderer()
.render(Gerrit.RESOURCES.sideBySideDiff()));
sbs.setTargetHistoryToken(
Dispatcher.toPatch("sidebyside", base, new Patch.Key(revision, path)));
sbs.setTitle(PatchUtil.C.sideBySideDiff());
linkPanel.add(sbs);
InlineHyperlink unified = new InlineHyperlink();
unified.setHTML(new ImageResourceRenderer()
.render(Gerrit.RESOURCES.unifiedDiff()));
unified.setTargetHistoryToken(
Dispatcher.toPatch("unified", base, new Patch.Key(revision, path)));
unified.setTitle(PatchUtil.C.unifiedDiff());
linkPanel.add(unified);
}
private Runnable updateCursorPosition() {
return new Runnable() {
@Override
public void run() {
// 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() {
cm.operation(new Runnable() {
@Override
public void run() {
updateActiveLine();
}
});
}
});
}
};
}
private void updateActiveLine() {
Pos p = cm.getCursor("end");
cursLine.setInnerText(Integer.toString(p.line() + 1));
cursCol.setInnerText(Integer.toString(p.ch() + 1));
cm.extras().activeLine(cm.getLineHandleVisualStart(p.line()));
}
private void setClean(boolean clean) {
save.setEnabled(!clean);
close.setEnabled(true);
dirty.getStyle().setVisibility(!clean ? VISIBLE : HIDDEN);
}
private Runnable save() {
return new Runnable() {
@Override
public void run() {
if (!cm.isClean(generation)) {
close.setEnabled(false);
String text = cm.getValue();
if (Patch.COMMIT_MSG.equals(path)) {
String trimmed = text.trim() + "\r";
if (!trimmed.equals(text)) {
text = trimmed;
cm.setValue(text);
}
}
final int g = cm.changeGeneration(false);
ChangeEditApi.put(revision.getParentKey().get(), path, text,
new GerritCallback<VoidResult>() {
@Override
public void onSuccess(VoidResult result) {
generation = g;
setClean(cm.isClean(g));
}
@Override
public void onFailure(final Throwable caught) {
close.setEnabled(true);
}
});
}
}
};
}
private void injectMode(String type, AsyncCallback<Void> cb) {
new ModeInjector().add(type).inject(cb);
}
}