/*******************************************************************************
* Copyright (C) 2010, 2015 Benjamin Muskalla <bmuskalla@eclipsesource.com> and others.
*
* 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
*
* Contributors:
* Benjamin Muskalla (EclipseSource) - initial implementation
* Tomasz Zarna (IBM) - show whitespace action, bug 371353
* Wayne Beaton (Eclipse Foundation) - Bug 433721
* Thomas Wolf (Paranor) - Hyperlink syntax coloring; bug 471355
*******************************************************************************/
package org.eclipse.egit.ui.internal.dialogs;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.egit.core.internal.Utils;
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.CommonUtils;
import org.eclipse.egit.ui.internal.UIText;
import org.eclipse.jface.action.Action;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.action.IMenuListener;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.Separator;
import org.eclipse.jface.action.SubMenuManager;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IPainter;
import org.eclipse.jface.text.ITextListener;
import org.eclipse.jface.text.ITextOperationTarget;
import org.eclipse.jface.text.ITextViewer;
import org.eclipse.jface.text.ITextViewerExtension2;
import org.eclipse.jface.text.MarginPainter;
import org.eclipse.jface.text.Position;
import org.eclipse.jface.text.TextEvent;
import org.eclipse.jface.text.WhitespaceCharacterPainter;
import org.eclipse.jface.text.contentassist.ICompletionProposal;
import org.eclipse.jface.text.contentassist.IContentAssistant;
import org.eclipse.jface.text.presentation.IPresentationReconciler;
import org.eclipse.jface.text.presentation.PresentationReconciler;
import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
import org.eclipse.jface.text.reconciler.IReconciler;
import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
import org.eclipse.jface.text.source.Annotation;
import org.eclipse.jface.text.source.AnnotationModel;
import org.eclipse.jface.text.source.IAnnotationAccess;
import org.eclipse.jface.text.source.IAnnotationModel;
import org.eclipse.jface.text.source.ISharedTextColors;
import org.eclipse.jface.text.source.ISourceViewer;
import org.eclipse.jface.text.source.SourceViewer;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jgit.util.IntList;
import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.BidiSegmentEvent;
import org.eclipse.swt.custom.BidiSegmentListener;
import org.eclipse.swt.custom.StyledText;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Layout;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.actions.ActionFactory;
import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
import org.eclipse.ui.editors.text.EditorsUI;
import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
import org.eclipse.ui.handlers.IHandlerService;
import org.eclipse.ui.texteditor.AbstractTextEditor;
import org.eclipse.ui.texteditor.AnnotationPreference;
import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
import org.eclipse.ui.texteditor.IUpdate;
import org.eclipse.ui.texteditor.MarkerAnnotationPreferences;
import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
/**
* Text field with support for spellchecking.
*/
public class SpellcheckableMessageArea extends Composite {
static final int MAX_LINE_WIDTH = 72;
private static class TextViewerAction extends Action implements IUpdate {
private int fOperationCode= -1;
private ITextOperationTarget fOperationTarget;
/**
* Creates a new action.
*
* @param target
* to operate on
* @param operationCode
* the opcode
*/
public TextViewerAction(ITextOperationTarget target,
int operationCode) {
fOperationCode= operationCode;
fOperationTarget = target;
update();
}
/**
* Updates the enabled state of the action.
*/
@Override
public void update() {
// XXX: workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=206111
if (fOperationCode == ITextOperationTarget.REDO) {
return;
}
setEnabled(fOperationTarget != null
&& fOperationTarget.canDoOperation(fOperationCode));
}
/**
* @see Action#run()
*/
@Override
public void run() {
if (fOperationCode != -1 && fOperationTarget != null)
fOperationTarget.doOperation(fOperationCode);
}
}
private static abstract class TextEditorPropertyAction extends Action implements IPropertyChangeListener {
private SourceViewer viewer;
private String preferenceKey;
private IPreferenceStore store;
public TextEditorPropertyAction(String label, SourceViewer viewer, String preferenceKey) {
super(label, IAction.AS_CHECK_BOX);
this.viewer = viewer;
this.preferenceKey = preferenceKey;
this.store = EditorsUI.getPreferenceStore();
if (store != null)
store.addPropertyChangeListener(this);
synchronizeWithPreference();
}
@Override
public void propertyChange(PropertyChangeEvent event) {
if (event.getProperty().equals(getPreferenceKey()))
synchronizeWithPreference();
}
protected void synchronizeWithPreference() {
boolean checked = false;
if (store != null)
checked = store.getBoolean(getPreferenceKey());
if (checked != isChecked()) {
setChecked(checked);
toggleState(checked);
} else if (checked) {
toggleState(false);
toggleState(true);
}
}
protected String getPreferenceKey() {
return preferenceKey;
}
@Override
public void run() {
toggleState(isChecked());
if (store != null)
store.setValue(getPreferenceKey(), isChecked());
}
public void dispose() {
if (store != null)
store.removePropertyChangeListener(this);
}
/**
* @param checked
* new state
*/
abstract protected void toggleState(boolean checked);
protected ITextViewer getTextViewer() {
return viewer;
}
protected IPreferenceStore getStore() {
return store;
}
}
private final SourceViewer sourceViewer;
private TextSourceViewerConfiguration configuration;
private BidiSegmentListener hardWrapSegmentListener;
// XXX: workaround for https://bugs.eclipse.org/400727
private int brokenBidiPlatformTextWidth;
private IAction contentAssistAction;
/**
* @param parent
* @param initialText
*/
public SpellcheckableMessageArea(Composite parent, String initialText) {
this(parent, initialText, SWT.BORDER);
}
/**
* @param parent
* @param initialText
* @param styles
*/
public SpellcheckableMessageArea(Composite parent, String initialText,
int styles) {
this(parent, initialText, false, styles);
}
/**
* @param parent
* @param initialText
* @param readOnly
* @param styles
*/
public SpellcheckableMessageArea(Composite parent, String initialText,
boolean readOnly,
int styles) {
super(parent, styles);
setLayout(new FillLayout());
AnnotationModel annotationModel = new AnnotationModel();
sourceViewer = new HyperlinkSourceViewer(this, null,
SWT.MULTI | SWT.V_SCROLL | SWT.WRAP);
getTextWidget().setAlwaysShowScrollBars(false);
getTextWidget().setFont(UIUtils
.getFont(UIPreferences.THEME_CommitMessageEditorFont));
sourceViewer.setDocument(new Document());
int endSpacing = 2;
int textWidth = getCharWidth() * MAX_LINE_WIDTH + endSpacing;
int textHeight = getLineHeight() * 7;
Point size = getTextWidget().computeSize(textWidth, textHeight);
getTextWidget().setSize(size);
computeBrokenBidiPlatformTextWidth(size.x);
getTextWidget().setEditable(!readOnly);
createMarginPainter();
configureHardWrap();
final IPropertyChangeListener propertyChangeListener = new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
if (UIPreferences.COMMIT_DIALOG_HARD_WRAP_MESSAGE.equals(event.getProperty())) {
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
configureHardWrap();
if (brokenBidiPlatformTextWidth != -1) {
layout();
}
}
});
}
}
};
Activator.getDefault().getPreferenceStore().addPropertyChangeListener(propertyChangeListener);
final IPropertyChangeListener syntaxColoringChangeListener = new IPropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent event) {
if (JFacePreferences.HYPERLINK_COLOR
.equals(event.getProperty())) {
getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (!isDisposed()) {
sourceViewer.refresh();
}
}
});
}
}
};
JFacePreferences.getPreferenceStore()
.addPropertyChangeListener(syntaxColoringChangeListener);
final SourceViewerDecorationSupport support = configureAnnotationPreferences();
Document document = new Document(initialText);
configuration = new HyperlinkSourceViewer.Configuration(
EditorsUI.getPreferenceStore()) {
@Override
public int getHyperlinkStateMask(ISourceViewer targetViewer) {
if (!targetViewer.isEditable()) {
return SWT.NONE;
}
return super.getHyperlinkStateMask(targetViewer);
}
@Override
protected Map getHyperlinkDetectorTargets(ISourceViewer targetViewer) {
return getHyperlinkTargets();
}
@Override
public IReconciler getReconciler(ISourceViewer viewer) {
if (!isEditable(viewer))
return null;
return super.getReconciler(sourceViewer);
}
@Override
public IContentAssistant getContentAssistant(ISourceViewer viewer) {
if (!viewer.isEditable())
return null;
IContentAssistant assistant = createContentAssistant(viewer);
// Add content assist proposal handler if assistant exists
if (assistant != null)
contentAssistAction = createContentAssistAction(
sourceViewer);
return assistant;
}
@Override
public IPresentationReconciler getPresentationReconciler(
ISourceViewer viewer) {
PresentationReconciler reconciler = new PresentationReconciler();
reconciler.setDocumentPartitioning(
getConfiguredDocumentPartitioning(viewer));
DefaultDamagerRepairer hyperlinkDamagerRepairer = new DefaultDamagerRepairer(
new HyperlinkTokenScanner(this, viewer));
reconciler.setDamager(hyperlinkDamagerRepairer,
IDocument.DEFAULT_CONTENT_TYPE);
reconciler.setRepairer(hyperlinkDamagerRepairer,
IDocument.DEFAULT_CONTENT_TYPE);
return reconciler;
}
};
sourceViewer.configure(configuration);
sourceViewer.setDocument(document, annotationModel);
configureContextMenu();
getTextWidget().addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent disposeEvent) {
support.uninstall();
Activator.getDefault().getPreferenceStore().removePropertyChangeListener(propertyChangeListener);
JFacePreferences.getPreferenceStore()
.removePropertyChangeListener(
syntaxColoringChangeListener);
}
});
}
private void computeBrokenBidiPlatformTextWidth(int textWidth) {
class BidiSegmentListenerTester implements BidiSegmentListener {
boolean called;
@Override
public void lineGetSegments(BidiSegmentEvent event) {
called = true;
}
}
BidiSegmentListenerTester tester = new BidiSegmentListenerTester();
StyledText textWidget = getTextWidget();
textWidget.addBidiSegmentListener(tester);
textWidget.setText(" "); //$NON-NLS-1$
textWidget.computeSize(SWT.DEFAULT, SWT.DEFAULT);
textWidget.removeBidiSegmentListener(tester);
brokenBidiPlatformTextWidth = tester.called ? -1 : textWidth;
}
private boolean isEditable(ISourceViewer viewer) {
return viewer != null && viewer.getTextWidget().getEditable();
}
private void configureHardWrap() {
if (shouldHardWrap()) {
if (hardWrapSegmentListener == null) {
final StyledText textWidget = getTextWidget();
hardWrapSegmentListener = new BidiSegmentListener() {
@Override
public void lineGetSegments(BidiSegmentEvent e) {
if (e.widget == textWidget) {
int footerOffset = CommonUtils
.getFooterOffset(textWidget.getText());
if (footerOffset >= 0
&& e.lineOffset >= footerOffset) {
return;
}
}
int[] segments = calculateWrapOffsets(e.lineText, MAX_LINE_WIDTH);
if (segments != null) {
char[] segmentsChars = new char[segments.length];
Arrays.fill(segmentsChars, '\n');
e.segments = segments;
e.segmentsChars = segmentsChars;
}
}
};
textWidget.addBidiSegmentListener(hardWrapSegmentListener);
textWidget.setText(textWidget.getText()); // XXX: workaround for https://bugs.eclipse.org/384886
if (brokenBidiPlatformTextWidth != -1) {
Layout restrictedWidthLayout = new Layout() {
@Override
protected Point computeSize(Composite composite,
int wHint, int hHint, boolean flushCache) {
Point size = SpellcheckableMessageArea.this
.getSize();
Rectangle trim = SpellcheckableMessageArea.this
.computeTrim(0, 0, 0, 0);
size.x -= trim.width;
size.y -= trim.height;
if (size.x > brokenBidiPlatformTextWidth)
size.x = brokenBidiPlatformTextWidth;
return size;
}
@Override
protected void layout(Composite composite,
boolean flushCache) {
Point size = computeSize(composite, SWT.DEFAULT,
SWT.DEFAULT, flushCache);
textWidget.setBounds(0, 0, size.x, size.y);
}
};
setLayout(restrictedWidthLayout);
}
}
} else if (hardWrapSegmentListener != null) {
StyledText textWidget = getTextWidget();
textWidget.removeBidiSegmentListener(hardWrapSegmentListener);
textWidget.setText(textWidget.getText()); // XXX: workaround for https://bugs.eclipse.org/384886
hardWrapSegmentListener = null;
if (brokenBidiPlatformTextWidth != -1)
setLayout(new FillLayout());
}
}
private TextViewerAction createFromActionFactory(ActionFactory factory,
int operationCode) {
IWorkbenchAction template = factory
.create(PlatformUI.getWorkbench().getActiveWorkbenchWindow());
TextViewerAction action = new TextViewerAction(sourceViewer,
operationCode);
action.setText(template.getText());
action.setImageDescriptor(template.getImageDescriptor());
action.setDisabledImageDescriptor(
template.getDisabledImageDescriptor());
action.setActionDefinitionId(template.getActionDefinitionId());
template.dispose();
return action;
}
private void configureContextMenu() {
final boolean editable = isEditable(sourceViewer);
TextViewerAction cutAction;
TextViewerAction undoAction;
TextViewerAction redoAction;
TextViewerAction pasteAction;
IAction quickFixAction;
if (editable) {
cutAction = createFromActionFactory(ActionFactory.CUT,
ITextOperationTarget.CUT);
undoAction = createFromActionFactory(ActionFactory.UNDO,
ITextOperationTarget.UNDO);
redoAction = createFromActionFactory(ActionFactory.REDO,
ITextOperationTarget.REDO);
pasteAction = createFromActionFactory(ActionFactory.PASTE,
ITextOperationTarget.PASTE);
quickFixAction = new QuickfixAction(sourceViewer);
} else {
cutAction = null;
undoAction = null;
redoAction = null;
pasteAction = null;
quickFixAction = null;
}
TextViewerAction copyAction = createFromActionFactory(
ActionFactory.COPY, ITextOperationTarget.COPY);
TextViewerAction selectAllAction = createFromActionFactory(
ActionFactory.SELECT_ALL, ITextOperationTarget.SELECT_ALL);
final TextEditorPropertyAction showWhitespaceAction = new TextEditorPropertyAction(
UIText.SpellcheckableMessageArea_showWhitespace,
sourceViewer,
AbstractTextEditor.PREFERENCE_SHOW_WHITESPACE_CHARACTERS) {
private IPainter whitespaceCharPainter;
@Override
public void propertyChange(PropertyChangeEvent event) {
String property = event.getProperty();
if (property.equals(getPreferenceKey())
|| AbstractTextEditor.PREFERENCE_SHOW_LEADING_SPACES
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_SPACES
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_TRAILING_SPACES
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_LEADING_IDEOGRAPHIC_SPACES
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_IDEOGRAPHIC_SPACES
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_TRAILING_IDEOGRAPHIC_SPACES
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_LEADING_TABS
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_TABS
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_TRAILING_TABS
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_CARRIAGE_RETURN
.equals(property)
|| AbstractTextEditor.PREFERENCE_SHOW_LINE_FEED
.equals(property)
|| AbstractTextEditor.PREFERENCE_WHITESPACE_CHARACTER_ALPHA_VALUE
.equals(property)) {
synchronizeWithPreference();
}
}
@Override
protected void toggleState(boolean checked) {
if (checked)
installPainter();
else
uninstallPainter();
}
/**
* Installs the painter on the viewer.
*/
private void installPainter() {
Assert.isTrue(whitespaceCharPainter == null);
ITextViewer v = getTextViewer();
if (v instanceof ITextViewerExtension2) {
IPreferenceStore store = getStore();
whitespaceCharPainter = new WhitespaceCharacterPainter(
v,
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LEADING_SPACES),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_SPACES),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_TRAILING_SPACES),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LEADING_IDEOGRAPHIC_SPACES),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_IDEOGRAPHIC_SPACES),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_TRAILING_IDEOGRAPHIC_SPACES),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LEADING_TABS),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_TABS),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_TRAILING_TABS),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_CARRIAGE_RETURN),
store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LINE_FEED),
store.getInt(AbstractTextEditor.PREFERENCE_WHITESPACE_CHARACTER_ALPHA_VALUE));
((ITextViewerExtension2) v).addPainter(whitespaceCharPainter);
}
}
/**
* Remove the painter from the viewer.
*/
private void uninstallPainter() {
if (whitespaceCharPainter == null)
return;
ITextViewer v = getTextViewer();
if (v instanceof ITextViewerExtension2)
((ITextViewerExtension2) v)
.removePainter(whitespaceCharPainter);
whitespaceCharPainter.deactivate(true);
whitespaceCharPainter = null;
}
};
MenuManager contextMenu = new MenuManager();
if (cutAction != null) {
contextMenu.add(cutAction);
}
contextMenu.add(copyAction);
if (pasteAction != null) {
contextMenu.add(pasteAction);
}
contextMenu.add(selectAllAction);
if (undoAction != null) {
contextMenu.add(undoAction);
}
if (redoAction != null) {
contextMenu.add(redoAction);
}
contextMenu.add(new Separator());
contextMenu.add(showWhitespaceAction);
contextMenu.add(new Separator());
if (editable) {
final SubMenuManager quickFixMenu = new SubMenuManager(contextMenu);
quickFixMenu.setVisible(true);
quickFixMenu.addMenuListener(new IMenuListener() {
@Override
public void menuAboutToShow(IMenuManager manager) {
quickFixMenu.removeAll();
addProposals(quickFixMenu);
}
});
}
final StyledText textWidget = getTextWidget();
List<IAction> globalActions = new ArrayList<>();
if (editable) {
globalActions.add(cutAction);
globalActions.add(pasteAction);
globalActions.add(undoAction);
globalActions.add(redoAction);
globalActions.add(quickFixAction);
}
globalActions.add(copyAction);
globalActions.add(selectAllAction);
if (contentAssistAction != null) {
globalActions.add(contentAssistAction);
}
ActionUtils.setGlobalActions(textWidget, globalActions,
getHandlerService());
textWidget.setMenu(contextMenu.createContextMenu(textWidget));
sourceViewer.addSelectionChangedListener(new ISelectionChangedListener() {
@Override
public void selectionChanged(SelectionChangedEvent event) {
if (cutAction != null)
cutAction.update();
copyAction.update();
}
});
if (editable) {
sourceViewer.addTextListener(new ITextListener() {
@Override
public void textChanged(TextEvent event) {
if (undoAction != null)
undoAction.update();
if (redoAction != null)
redoAction.update();
}
});
}
textWidget.addDisposeListener(new DisposeListener() {
@Override
public void widgetDisposed(DisposeEvent disposeEvent) {
showWhitespaceAction.dispose();
}
});
}
private void addProposals(final SubMenuManager quickFixMenu) {
IAnnotationModel sourceModel = sourceViewer.getAnnotationModel();
if (sourceModel == null) {
return;
}
Iterator annotationIterator = sourceModel.getAnnotationIterator();
while (annotationIterator.hasNext()) {
Annotation annotation = (Annotation) annotationIterator.next();
boolean isDeleted = annotation.isMarkedDeleted();
boolean isIncluded = !isDeleted
&& includes(sourceModel.getPosition(annotation),
getTextWidget().getCaretOffset());
boolean isFixable = isIncluded && sourceViewer
.getQuickAssistAssistant().canFix(annotation);
if (isFixable) {
IQuickAssistProcessor processor = sourceViewer
.getQuickAssistAssistant().getQuickAssistProcessor();
IQuickAssistInvocationContext context = sourceViewer
.getQuickAssistInvocationContext();
ICompletionProposal[] proposals = processor
.computeQuickAssistProposals(context);
for (ICompletionProposal proposal : proposals) {
quickFixMenu.add(createQuickFixAction(proposal));
}
}
}
}
private boolean includes(Position position, int caretOffset) {
return position != null && (position.includes(caretOffset)
|| (position.offset + position.length) == caretOffset);
}
private IAction createQuickFixAction(final ICompletionProposal proposal) {
return new Action(proposal.getDisplayString()) {
@Override
public void run() {
proposal.apply(sourceViewer.getDocument());
}
@Override
public ImageDescriptor getImageDescriptor() {
Image image = proposal.getImage();
if (image != null)
return ImageDescriptor.createFromImage(image);
return null;
}
};
}
/**
* Return <code>IHandlerService</code>. The default implementation uses the
* workbench window's service locator. Subclasses may override to access the
* service by using a local service locator.
*
* @return <code>IHandlerService</code> using the workbench window's service
* locator. Can be <code>null</code> if the service could not be
* found.
*/
protected IHandlerService getHandlerService() {
return CommonUtils.getService(PlatformUI.getWorkbench(), IHandlerService.class);
}
private SourceViewerDecorationSupport configureAnnotationPreferences() {
ISharedTextColors textColors = EditorsUI.getSharedTextColors();
IAnnotationAccess annotationAccess = new DefaultMarkerAnnotationAccess();
final SourceViewerDecorationSupport support = new SourceViewerDecorationSupport(
sourceViewer, null, annotationAccess, textColors);
List annotationPreferences = new MarkerAnnotationPreferences()
.getAnnotationPreferences();
Iterator e = annotationPreferences.iterator();
while (e.hasNext())
support.setAnnotationPreference((AnnotationPreference) e.next());
support.install(EditorsUI.getPreferenceStore());
return support;
}
/**
* Create margin painter and add to source viewer
*/
protected void createMarginPainter() {
MarginPainter marginPainter = new MarginPainter(sourceViewer);
marginPainter.setMarginRulerColumn(MAX_LINE_WIDTH);
marginPainter.setMarginRulerColor(Display.getDefault().getSystemColor(
SWT.COLOR_GRAY));
sourceViewer.addPainter(marginPainter);
}
private int getCharWidth() {
GC gc = new GC(getTextWidget());
int charWidth = gc.getFontMetrics().getAverageCharWidth();
gc.dispose();
return charWidth;
}
private int getLineHeight() {
return getTextWidget().getLineHeight();
}
/**
* @return if the commit message should be hard-wrapped (preference)
*/
private static boolean shouldHardWrap() {
return Activator.getDefault().getPreferenceStore()
.getBoolean(UIPreferences.COMMIT_DIALOG_HARD_WRAP_MESSAGE);
}
/**
* @return widget
*/
public StyledText getTextWidget() {
return sourceViewer.getTextWidget();
}
private static class QuickfixAction extends Action {
private final ITextOperationTarget textOperationTarget;
public QuickfixAction(ITextOperationTarget target) {
textOperationTarget = target;
setActionDefinitionId(
ITextEditorActionDefinitionIds.QUICK_ASSIST);
}
@Override
public void run() {
if (textOperationTarget.canDoOperation(ISourceViewer.QUICK_ASSIST)) {
textOperationTarget.doOperation(ISourceViewer.QUICK_ASSIST);
}
}
}
private IAction createContentAssistAction(
final SourceViewer viewer) {
Action proposalAction = new TextViewerAction(viewer,
ISourceViewer.CONTENTASSIST_PROPOSALS);
proposalAction
.setActionDefinitionId(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
return proposalAction;
}
/**
* Returns the commit message, converting platform-specific line endings to '\n'
* and hard-wrapping lines if necessary.
*
* @return commit message
*/
public String getCommitMessage() {
String text = getText();
text = Utils.normalizeLineEndings(text);
if (shouldHardWrap()) {
text = wrapCommitMessage(text);
}
return text;
}
/**
* Wraps a commit message, leaving the footer as defined by
* {@link CommonUtils#getFooterOffset(String)} unwrapped.
*
* @param text
* of the whole commit message, including footer, using '\n' as
* line delimiter
* @return the wrapped text
*/
protected static String wrapCommitMessage(String text) {
// protected in order to be easily testable
int footerStart = CommonUtils.getFooterOffset(text);
if (footerStart < 0) {
return hardWrap(text);
} else {
// Do not wrap footer lines.
String footer = text.substring(footerStart);
text = hardWrap(text.substring(0, footerStart));
return text + footer;
}
}
/**
* Hard-wraps the given text.
*
* @param text
* the text to wrap, must use '\n' as line delimiter
* @return the wrapped text
*/
protected static String hardWrap(String text) {
// protected for testing
int[] wrapOffsets = calculateWrapOffsets(text, MAX_LINE_WIDTH);
if (wrapOffsets != null) {
StringBuilder builder = new StringBuilder(text.length() + wrapOffsets.length);
int prev = 0;
for (int cur : wrapOffsets) {
builder.append(text.substring(prev, cur));
for (int j = cur; j > prev && builder.charAt(builder.length() - 1) == ' '; j--)
builder.deleteCharAt(builder.length() - 1);
builder.append('\n');
prev = cur;
}
builder.append(text.substring(prev));
return builder.toString();
}
return text;
}
/**
* Get hyperlink targets
*
* @return map of targets
*/
protected Map<String, IAdaptable> getHyperlinkTargets() {
return Collections.singletonMap(EditorsUI.DEFAULT_TEXT_EDITOR_ID,
getDefaultTarget());
}
/**
* Create content assistant
*
* @param viewer
* @return content assistant
*/
protected IContentAssistant createContentAssistant(ISourceViewer viewer) {
return null;
}
/**
* Get default target for hyperlink presenter
*
* @return target
*/
protected IAdaptable getDefaultTarget() {
return null;
}
/**
* @return text
*/
public String getText() {
return getDocument().get();
}
/**
* @return document
*/
public IDocument getDocument() {
return sourceViewer.getDocument();
}
/**
* @param text
*/
public void setText(String text) {
if (text != null) {
getDocument().set(text);
}
}
/**
* Set the same background color to the styledText widget as the Composite
*/
@Override
public void setBackground(Color color) {
super.setBackground(color);
StyledText textWidget = getTextWidget();
textWidget.setBackground(color);
}
/**
*
*/
@Override
public boolean forceFocus() {
return getTextWidget().setFocus();
}
/**
* Calculates wrap offsets for the given line, so that resulting lines are
* no longer than <code>maxLineLength</code> if possible.
*
* @param line
* the line to wrap (can contain '\n', but no other line delimiters)
* @param maxLineLength
* the maximum line length
* @return an array of offsets where hard-wraps should be inserted, or
* <code>null</code> if the line does not need to be wrapped
*/
public static int[] calculateWrapOffsets(final String line, final int maxLineLength) {
if (line.length() == 0)
return null;
IntList wrapOffsets = new IntList();
int wordStart = 0;
int lineStart = 0;
boolean lastWasSpace = true;
boolean onlySpaces = true;
for (int i = 0; i < line.length(); i++) {
char ch = line.charAt(i);
if (ch == ' ') {
lastWasSpace = true;
} else if (ch == '\n') {
lineStart = i + 1;
wordStart = i + 1;
lastWasSpace = true;
onlySpaces = true;
} else { // a word character
if (lastWasSpace) {
lastWasSpace = false;
if (!onlySpaces) { // don't break line with <spaces><veryLongWord>
wordStart = i;
}
} else {
onlySpaces = false;
}
if (i >= lineStart + maxLineLength) {
if (wordStart != lineStart) { // don't break before a single long word
wrapOffsets.add(wordStart);
lineStart = wordStart;
onlySpaces = true;
}
}
}
}
int size = wrapOffsets.size();
if (size == 0) {
return null;
} else {
int[] result = new int[size];
for (int i = 0; i < size; i++) {
result[i] = wrapOffsets.get(i);
}
return result;
}
}
}