/*******************************************************************************
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* Copyright (C) 2011, Mathias Kinzler <mathias.kinzler@sap.com>
* Copyright (C) 2011, Jens Baumgart <jens.baumgart@sap.com>
* Copyright (C) 2011, Stefan Lay <stefan.lay@sap.com>
* Copyright (C) 2014, Marc-Andre Laperle <marc-andre.laperle@ericsson.com>
* Copyright (C) 2015, IBM Corporation (Dani Megert <daniel_megert@ch.ibm.com>)
* Copyright (C) 2015, 2016 Thomas Wolf <thomas.wolf@paranor.ch>
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*******************************************************************************/
package org.eclipse.egit.ui.internal.history;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.core.runtime.ListenerList;
import org.eclipse.core.runtime.jobs.IJobChangeEvent;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.core.runtime.jobs.JobChangeAdapter;
import org.eclipse.egit.core.AdapterUtils;
import org.eclipse.egit.ui.Activator;
import org.eclipse.egit.ui.UIPreferences;
import org.eclipse.egit.ui.UIUtils;
import org.eclipse.egit.ui.internal.ActionUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.egit.ui.internal.actions.BooleanPrefAction;
import org.eclipse.egit.ui.internal.dialogs.HyperlinkSourceViewer;
import org.eclipse.egit.ui.internal.history.FormatJob.FormatResult;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.preference.IPersistentPreferenceStore;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.text.DefaultTextDoubleClickStrategy;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IDocumentPartitioner;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.TextUtilities;
import org.eclipse.jface.text.hyperlink.IHyperlink;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetector;
import org.eclipse.jface.text.hyperlink.IHyperlinkDetectorExtension2;
import org.eclipse.jface.text.rules.FastPartitioner;
import org.eclipse.jface.text.rules.IPartitionTokenScanner;
import org.eclipse.jface.text.rules.IToken;
import org.eclipse.jface.text.rules.Token;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jgit.events.ListenerHandle;
import org.eclipse.jgit.events.RefsChangedEvent;
import org.eclipse.jgit.events.RefsChangedListener;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefDatabase;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.ui.IWorkbenchPartSite;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.progress.IWorkbenchSiteProgressService;
class CommitMessageViewer extends HyperlinkSourceViewer {
static final String HEADER_CONTENT_TYPE = "__egit_commit_msg_header"; //$NON-NLS-1$
static final String FOOTER_CONTENT_TYPE = "__egit_commit_msg_footer"; //$NON-NLS-1$
// notified when clicking on a link in the message (branch, commit...)
private final ListenerList navListeners = new ListenerList();
// listener to detect changes in the wrap and fill preferences
private final IPropertyChangeListener listener;
// Listener to react on syntax coloring preferences changes
private final IPropertyChangeListener syntaxColoringListener;
// the current repository
private Repository db;
// the "input" (set by setInput())
private PlotCommit<?> commit;
// formatting option to fill the lines
private boolean fill;
private FormatJob formatJob;
private final IWorkbenchPartSite partSite;
private List<Ref> allRefs;
private ListenerHandle refsChangedListener;
private BooleanPrefAction showTagSequencePrefAction;
private BooleanPrefAction showBranchSequencePrefAction;
private BooleanPrefAction wrapCommentsPrefAction;
private BooleanPrefAction fillParagraphsPrefAction;
CommitMessageViewer(final Composite parent, IWorkbenchPartSite partSite) {
super(parent, null, SWT.READ_ONLY);
this.partSite = partSite;
final StyledText t = getTextWidget();
t.setFont(UIUtils.getFont(UIPreferences.THEME_CommitMessageFont));
setTextDoubleClickStrategy(new DefaultTextDoubleClickStrategy(),
IDocument.DEFAULT_CONTENT_TYPE);
activatePlugins();
// react on changes in the fill and wrap preferences
listener = new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
String property = event.getProperty();
if (UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_FILL
.equals(property)) {
setFill(((Boolean) event.getNewValue()).booleanValue());
} else
if (UIPreferences.HISTORY_SHOW_TAG_SEQUENCE.equals(property)
|| UIPreferences.HISTORY_SHOW_BRANCH_SEQUENCE
.equals(property)
|| UIPreferences.DATE_FORMAT.equals(property)
|| UIPreferences.DATE_FORMAT_CHOICE
.equals(property)) {
format();
}
}
};
IPreferenceStore store = Activator.getDefault().getPreferenceStore();
store.addPropertyChangeListener(listener);
fill = store
.getBoolean(UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_FILL);
// React on changes in the JFace color preferences by updating the view
syntaxColoringListener = new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
if (JFacePreferences.HYPERLINK_COLOR
.equals(event.getProperty())) {
if (!t.isDisposed()) {
t.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (!t.isDisposed()) {
refresh();
}
}
});
}
}
}
};
JFacePreferences.getPreferenceStore()
.addPropertyChangeListener(syntaxColoringListener);
// global action handlers for select all and copy
final IAction selectAll = ActionUtils.createGlobalAction(
ActionFactory.SELECT_ALL,
() -> doOperation(ITextOperationTarget.SELECT_ALL),
() -> canDoOperation(ITextOperationTarget.SELECT_ALL));
final IAction copy = ActionUtils.createGlobalAction(ActionFactory.COPY,
() -> doOperation(ITextOperationTarget.COPY),
() -> canDoOperation(ITextOperationTarget.COPY));
ActionUtils.setGlobalActions(getControl(), copy, selectAll);
final MenuManager mgr = new MenuManager();
Control c = getControl();
c.setMenu(mgr.createContextMenu(c));
IPersistentPreferenceStore pstore = (IPersistentPreferenceStore) store;
showBranchSequencePrefAction = new BooleanPrefAction(pstore,
UIPreferences.HISTORY_SHOW_BRANCH_SEQUENCE,
UIText.ResourceHistory_ShowBranchSequence) {
@Override
protected void apply(boolean value) {
// nothing, just toggle
}
};
mgr.add(showBranchSequencePrefAction);
showTagSequencePrefAction = new BooleanPrefAction(pstore,
UIPreferences.HISTORY_SHOW_TAG_SEQUENCE,
UIText.ResourceHistory_ShowTagSequence) {
@Override
protected void apply(boolean value) {
// nothing, just toggle
}
};
mgr.add(showTagSequencePrefAction);
wrapCommentsPrefAction = new BooleanPrefAction(pstore,
UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_WRAP,
UIText.ResourceHistory_toggleCommentWrap) {
@Override
protected void apply(boolean value) {
// nothing, just toggle
}
};
mgr.add(wrapCommentsPrefAction);
fillParagraphsPrefAction = new BooleanPrefAction(pstore,
UIPreferences.RESOURCEHISTORY_SHOW_COMMENT_FILL,
UIText.ResourceHistory_toggleCommentFill) {
@Override
protected void apply(boolean value) {
// nothing, just toggle
}
};
mgr.add(fillParagraphsPrefAction);
}
void addDoneListenerToFormatJob() {
formatJob.addJobChangeListener(new JobChangeAdapter() {
@Override
public void done(IJobChangeEvent event) {
if (!event.getResult().isOK())
return;
final StyledText text = getTextWidget();
if (text == null || text.isDisposed())
return;
final FormatResult result = ((FormatJob) event.getJob())
.getFormatResult();
text.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
applyFormatJobResultInUI(result);
}
});
}
});
}
@Override
protected void handleDispose() {
if (formatJob != null) {
formatJob.cancel();
formatJob = null;
}
Activator.getDefault().getPreferenceStore()
.removePropertyChangeListener(listener);
JFacePreferences.getPreferenceStore()
.removePropertyChangeListener(syntaxColoringListener);
if (refsChangedListener != null)
refsChangedListener.remove();
refsChangedListener = null;
showBranchSequencePrefAction.dispose();
showTagSequencePrefAction.dispose();
wrapCommentsPrefAction.dispose();
fillParagraphsPrefAction.dispose();
super.handleDispose();
}
void addCommitNavigationListener(final CommitNavigationListener l) {
navListeners.add(l);
}
void removeCommitNavigationListener(final CommitNavigationListener l) {
navListeners.remove(l);
}
@Override
public void setInput(final Object input) {
// right-clicking on a commit will fire selection change events,
// so we only rebuild this when the commit did in fact change
if (input == commit)
return;
commit = (PlotCommit<?>) input;
if (refsChangedListener != null) {
refsChangedListener.remove();
refsChangedListener = null;
}
if (db != null) {
allRefs = getBranches(db);
refsChangedListener = db.getListenerList().addRefsChangedListener(
new RefsChangedListener() {
@Override
public void onRefsChanged(RefsChangedEvent event) {
allRefs = getBranches(db);
}
});
}
format();
}
@Override
public Object getInput() {
return commit;
}
void setRepository(final Repository repository) {
this.db = repository;
}
private static List<Ref> getBranches(Repository repo) {
List<Ref> ref = new ArrayList<>();
try {
RefDatabase refDb = repo.getRefDatabase();
ref.addAll(refDb.getRefs(Constants.R_HEADS).values());
ref.addAll(refDb.getRefs(Constants.R_REMOTES).values());
} catch (IOException e) {
Activator.logError(e.getMessage(), e);
}
return ref;
}
private Repository getRepository() {
if (db == null)
throw new IllegalStateException("Repository has not been set"); //$NON-NLS-1$
return db;
}
private void format() {
if (db == null || commit == null) {
setDocument(new Document("")); //$NON-NLS-1$
return;
}
if (formatJob != null && formatJob.getState() != Job.NONE)
formatJob.cancel();
scheduleFormatJob();
}
private void scheduleFormatJob() {
IWorkbenchSiteProgressService siteService = AdapterUtils.adapt(partSite, IWorkbenchSiteProgressService.class);
if (siteService == null)
return;
FormatJob.FormatRequest formatRequest = new FormatJob.FormatRequest(
getRepository(), commit, fill, allRefs);
formatJob = new FormatJob(formatRequest);
addDoneListenerToFormatJob();
siteService.schedule(formatJob, 0 /* now */, true /*
* use the half-busy
* cursor in the part
*/);
}
private void applyFormatJobResultInUI(FormatResult formatResult) {
StyledText text = getTextWidget();
if (!UIUtils.isUsable(text))
return;
setDocument(new CommitDocument(formatResult));
}
private class ObjectHyperlink implements IHyperlink {
private final GitCommitReference link;
public ObjectHyperlink(GitCommitReference link) {
this.link = link;
}
@Override
public IRegion getHyperlinkRegion() {
return link.getRegion();
}
@Override
public String getTypeLabel() {
return null;
}
@Override
public String getHyperlinkText() {
return link.getTarget().name();
}
@Override
public void open() {
for (final Object l : navListeners.getListeners()) {
((CommitNavigationListener) l).showCommit(link.getTarget());
}
}
}
private class CommitDocument extends Document {
private final List<IHyperlink> hyperlinks;
private final int headerEnd;
private final int footerStart;
public CommitDocument(FormatResult format) {
super(format.getCommitInfo());
headerEnd = format.getHeaderEnd();
footerStart = format.getFooterStart();
List<GitCommitReference> knownLinks = format.getKnownLinks();
hyperlinks = new ArrayList<>(knownLinks.size());
for (GitCommitReference o : knownLinks) {
hyperlinks.add(new ObjectHyperlink(o));
}
IDocumentPartitioner partitioner = new FastPartitioner(
new CommitPartitionTokenScanner(),
new String[] { IDocument.DEFAULT_CONTENT_TYPE,
HEADER_CONTENT_TYPE, FOOTER_CONTENT_TYPE });
partitioner.connect(this);
this.setDocumentPartitioner(partitioner);
}
public List<IHyperlink> getKnownHyperlinks() {
return hyperlinks;
}
public int getHeaderEnd() {
return headerEnd;
}
public int getFooterStart() {
return footerStart;
}
}
private static class CommitPartitionTokenScanner
implements IPartitionTokenScanner {
private static final IToken HEADER = new Token(HEADER_CONTENT_TYPE);
private static final IToken BODY = new Token(
IDocument.DEFAULT_CONTENT_TYPE);
private static final IToken FOOTER = new Token(FOOTER_CONTENT_TYPE);
private int headerEnd;
private int footerStart;
private int currentOffset;
private int end;
private int tokenStart;
@Override
public void setRange(IDocument document, int offset, int length) {
if (document instanceof CommitDocument) {
CommitDocument d = (CommitDocument) document;
headerEnd = d.getHeaderEnd();
footerStart = d.getFooterStart();
} else {
headerEnd = 0;
footerStart = document.getLength();
}
currentOffset = offset;
end = offset + length;
tokenStart = -1;
}
@Override
public IToken nextToken() {
tokenStart = currentOffset;
if (currentOffset < end) {
if (currentOffset < headerEnd) {
currentOffset = Math.min(headerEnd, end);
return HEADER;
} else if (currentOffset < footerStart) {
currentOffset = Math.min(footerStart, end);
return BODY;
} else {
currentOffset = end;
return FOOTER;
}
}
return Token.EOF;
}
@Override
public int getTokenOffset() {
return tokenStart;
}
@Override
public int getTokenLength() {
return currentOffset - tokenStart;
}
@Override
public void setPartialRange(IDocument document, int offset, int length,
String contentType, int partitionOffset) {
setRange(document, offset, length);
}
}
static class KnownHyperlinksDetector
implements IHyperlinkDetector, IHyperlinkDetectorExtension2 {
@Override
public IHyperlink[] detectHyperlinks(ITextViewer textViewer,
IRegion region, boolean canShowMultipleHyperlinks) {
IDocument document = textViewer.getDocument();
if (document instanceof CommitDocument) {
List<IHyperlink> knownLinks = ((CommitDocument) document)
.getKnownHyperlinks();
List<IHyperlink> result = new ArrayList<>();
for (IHyperlink link : knownLinks) {
IRegion linkRegion = link.getHyperlinkRegion();
if (TextUtilities.overlaps(linkRegion, region)) {
result.add(link);
}
}
if (!result.isEmpty()) {
return result.toArray(new IHyperlink[result.size()]);
}
}
return null;
}
@Override
public int getStateMask() {
return -1;
}
}
private void setFill(boolean fill) {
this.fill = fill;
format();
}
}