Improve commit message validation & remove leading whitespace
[egit/eclipse.git] / org.eclipse.egit.ui / src / org / eclipse / egit / ui / internal / dialogs / SpellcheckableMessageArea.java
blobb0e1101191475eac0bbdb3c189b6cf63dd5050c8
1 /*******************************************************************************
2 * Copyright (C) 2010, 2015 Benjamin Muskalla <bmuskalla@eclipsesource.com> and others.
4 * All rights reserved. This program and the accompanying materials
5 * are made available under the terms of the Eclipse Public License v1.0
6 * which accompanies this distribution, and is available at
7 * http://www.eclipse.org/legal/epl-v10.html
9 * Contributors:
10 * Benjamin Muskalla (EclipseSource) - initial implementation
11 * Tomasz Zarna (IBM) - show whitespace action, bug 371353
12 * Wayne Beaton (Eclipse Foundation) - Bug 433721
13 * Thomas Wolf (Paranor) - Hyperlink syntax coloring; bug 471355
14 *******************************************************************************/
15 package org.eclipse.egit.ui.internal.dialogs;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collections;
20 import java.util.Iterator;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.regex.Pattern;
25 import org.eclipse.core.runtime.Assert;
26 import org.eclipse.core.runtime.IAdaptable;
27 import org.eclipse.egit.core.internal.Utils;
28 import org.eclipse.egit.ui.Activator;
29 import org.eclipse.egit.ui.UIPreferences;
30 import org.eclipse.egit.ui.UIUtils;
31 import org.eclipse.egit.ui.internal.ActionUtils;
32 import org.eclipse.egit.ui.internal.CommonUtils;
33 import org.eclipse.egit.ui.internal.UIText;
34 import org.eclipse.jface.action.Action;
35 import org.eclipse.jface.action.IAction;
36 import org.eclipse.jface.action.IMenuListener;
37 import org.eclipse.jface.action.IMenuManager;
38 import org.eclipse.jface.action.MenuManager;
39 import org.eclipse.jface.action.Separator;
40 import org.eclipse.jface.action.SubMenuManager;
41 import org.eclipse.jface.preference.IPreferenceStore;
42 import org.eclipse.jface.resource.ImageDescriptor;
43 import org.eclipse.jface.resource.JFaceResources;
44 import org.eclipse.jface.text.Document;
45 import org.eclipse.jface.text.IDocument;
46 import org.eclipse.jface.text.IPainter;
47 import org.eclipse.jface.text.ITextListener;
48 import org.eclipse.jface.text.ITextOperationTarget;
49 import org.eclipse.jface.text.ITextViewer;
50 import org.eclipse.jface.text.ITextViewerExtension2;
51 import org.eclipse.jface.text.MarginPainter;
52 import org.eclipse.jface.text.Position;
53 import org.eclipse.jface.text.TextEvent;
54 import org.eclipse.jface.text.WhitespaceCharacterPainter;
55 import org.eclipse.jface.text.contentassist.ICompletionProposal;
56 import org.eclipse.jface.text.contentassist.IContentAssistant;
57 import org.eclipse.jface.text.presentation.IPresentationReconciler;
58 import org.eclipse.jface.text.presentation.PresentationReconciler;
59 import org.eclipse.jface.text.quickassist.IQuickAssistInvocationContext;
60 import org.eclipse.jface.text.quickassist.IQuickAssistProcessor;
61 import org.eclipse.jface.text.reconciler.IReconciler;
62 import org.eclipse.jface.text.rules.DefaultDamagerRepairer;
63 import org.eclipse.jface.text.source.Annotation;
64 import org.eclipse.jface.text.source.AnnotationModel;
65 import org.eclipse.jface.text.source.IAnnotationAccess;
66 import org.eclipse.jface.text.source.IAnnotationModel;
67 import org.eclipse.jface.text.source.ISharedTextColors;
68 import org.eclipse.jface.text.source.ISourceViewer;
69 import org.eclipse.jface.text.source.SourceViewer;
70 import org.eclipse.jface.util.IPropertyChangeListener;
71 import org.eclipse.jface.util.PropertyChangeEvent;
72 import org.eclipse.jface.viewers.ISelectionChangedListener;
73 import org.eclipse.jface.viewers.SelectionChangedEvent;
74 import org.eclipse.jgit.util.IntList;
75 import org.eclipse.swt.SWT;
76 import org.eclipse.swt.custom.BidiSegmentEvent;
77 import org.eclipse.swt.custom.BidiSegmentListener;
78 import org.eclipse.swt.custom.StyledText;
79 import org.eclipse.swt.events.DisposeEvent;
80 import org.eclipse.swt.events.DisposeListener;
81 import org.eclipse.swt.graphics.Color;
82 import org.eclipse.swt.graphics.Font;
83 import org.eclipse.swt.graphics.GC;
84 import org.eclipse.swt.graphics.Image;
85 import org.eclipse.swt.graphics.Point;
86 import org.eclipse.swt.graphics.Rectangle;
87 import org.eclipse.swt.layout.FillLayout;
88 import org.eclipse.swt.widgets.Composite;
89 import org.eclipse.swt.widgets.Display;
90 import org.eclipse.swt.widgets.Layout;
91 import org.eclipse.ui.PlatformUI;
92 import org.eclipse.ui.actions.ActionFactory;
93 import org.eclipse.ui.actions.ActionFactory.IWorkbenchAction;
94 import org.eclipse.ui.editors.text.EditorsUI;
95 import org.eclipse.ui.editors.text.TextSourceViewerConfiguration;
96 import org.eclipse.ui.handlers.IHandlerService;
97 import org.eclipse.ui.texteditor.AbstractTextEditor;
98 import org.eclipse.ui.texteditor.AnnotationPreference;
99 import org.eclipse.ui.texteditor.DefaultMarkerAnnotationAccess;
100 import org.eclipse.ui.texteditor.ITextEditorActionDefinitionIds;
101 import org.eclipse.ui.texteditor.IUpdate;
102 import org.eclipse.ui.texteditor.MarkerAnnotationPreferences;
103 import org.eclipse.ui.texteditor.SourceViewerDecorationSupport;
104 import org.eclipse.ui.themes.IThemeManager;
107 * Text field with support for spellchecking.
109 public class SpellcheckableMessageArea extends Composite {
111 static final int MAX_LINE_WIDTH = 72;
113 private static final Pattern TRAILING_WHITE_SPACE_ON_LINES = Pattern
114 .compile("\\h+$", Pattern.MULTILINE); //$NON-NLS-1$
116 private static final Pattern TRAILING_NEWLINES = Pattern.compile("\\v+$"); //$NON-NLS-1$
118 private static class TextViewerAction extends Action implements IUpdate {
120 private int fOperationCode= -1;
121 private ITextOperationTarget fOperationTarget;
124 * Creates a new action.
126 * @param target
127 * to operate on
128 * @param operationCode
129 * the opcode
131 public TextViewerAction(ITextOperationTarget target,
132 int operationCode) {
133 fOperationCode= operationCode;
134 fOperationTarget = target;
135 update();
139 * Updates the enabled state of the action.
141 @Override
142 public void update() {
143 // XXX: workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=206111
144 if (fOperationCode == ITextOperationTarget.REDO) {
145 return;
147 setEnabled(fOperationTarget != null
148 && fOperationTarget.canDoOperation(fOperationCode));
152 * @see Action#run()
154 @Override
155 public void run() {
156 if (fOperationCode != -1 && fOperationTarget != null)
157 fOperationTarget.doOperation(fOperationCode);
161 private static abstract class TextEditorPropertyAction extends Action implements IPropertyChangeListener {
163 private SourceViewer viewer;
164 private String preferenceKey;
165 private IPreferenceStore store;
167 public TextEditorPropertyAction(String label, SourceViewer viewer, String preferenceKey) {
168 super(label, IAction.AS_CHECK_BOX);
169 this.viewer = viewer;
170 this.preferenceKey = preferenceKey;
171 this.store = EditorsUI.getPreferenceStore();
172 if (store != null)
173 store.addPropertyChangeListener(this);
174 synchronizeWithPreference();
177 @Override
178 public void propertyChange(PropertyChangeEvent event) {
179 if (event.getProperty().equals(getPreferenceKey()))
180 synchronizeWithPreference();
183 protected void synchronizeWithPreference() {
184 boolean checked = false;
185 if (store != null)
186 checked = store.getBoolean(getPreferenceKey());
187 if (checked != isChecked()) {
188 setChecked(checked);
189 toggleState(checked);
190 } else if (checked) {
191 toggleState(false);
192 toggleState(true);
196 protected String getPreferenceKey() {
197 return preferenceKey;
200 @Override
201 public void run() {
202 toggleState(isChecked());
203 if (store != null)
204 store.setValue(getPreferenceKey(), isChecked());
207 public void dispose() {
208 if (store != null)
209 store.removePropertyChangeListener(this);
213 * @param checked
214 * new state
216 abstract protected void toggleState(boolean checked);
218 protected ITextViewer getTextViewer() {
219 return viewer;
222 protected IPreferenceStore getStore() {
223 return store;
227 private final HyperlinkSourceViewer sourceViewer;
229 private TextSourceViewerConfiguration configuration;
231 private BidiSegmentListener hardWrapSegmentListener;
233 // XXX: workaround for https://bugs.eclipse.org/400727
234 private int brokenBidiPlatformTextWidth;
236 private IAction contentAssistAction;
239 * @param parent
240 * @param initialText
242 public SpellcheckableMessageArea(Composite parent, String initialText) {
243 this(parent, initialText, SWT.BORDER);
247 * @param parent
248 * @param initialText
249 * @param styles
251 public SpellcheckableMessageArea(Composite parent, String initialText,
252 int styles) {
253 this(parent, initialText, false, styles);
257 * @param parent
258 * @param initialText
259 * @param readOnly
260 * @param styles
262 public SpellcheckableMessageArea(Composite parent, String initialText,
263 boolean readOnly, int styles) {
264 super(parent, styles);
265 setLayout(new FillLayout());
267 AnnotationModel annotationModel = new AnnotationModel();
268 sourceViewer = new HyperlinkSourceViewer(this, null,
269 SWT.MULTI | SWT.V_SCROLL | SWT.WRAP) {
270 @Override
271 protected void handleJFacePreferencesChange(
272 PropertyChangeEvent event) {
273 if (JFaceResources.TEXT_FONT.equals(event.getProperty())) {
274 Font themeFont = UIUtils.getFont(
275 UIPreferences.THEME_CommitMessageEditorFont);
276 Font jFaceFont = JFaceResources.getTextFont();
277 if (themeFont.equals(jFaceFont)) {
278 setFont(jFaceFont);
280 } else {
281 super.handleJFacePreferencesChange(event);
285 getTextWidget().setAlwaysShowScrollBars(false);
286 getTextWidget().setFont(UIUtils
287 .getFont(UIPreferences.THEME_CommitMessageEditorFont));
288 sourceViewer.setDocument(new Document());
289 int endSpacing = 2;
290 int textWidth = getCharWidth() * MAX_LINE_WIDTH + endSpacing;
291 int textHeight = getLineHeight() * 7;
292 Point size = getTextWidget().computeSize(textWidth, textHeight);
293 getTextWidget().setSize(size);
295 computeBrokenBidiPlatformTextWidth(size.x);
297 getTextWidget().setEditable(!readOnly);
299 createMarginPainter();
301 configureHardWrap();
303 final IPropertyChangeListener propertyChangeListener = event -> {
304 if (UIPreferences.COMMIT_DIALOG_HARD_WRAP_MESSAGE
305 .equals(event.getProperty())) {
306 getDisplay().asyncExec(() -> {
307 if (!isDisposed()) {
308 configureHardWrap();
309 if (brokenBidiPlatformTextWidth != -1) {
310 layout();
316 Activator.getDefault().getPreferenceStore().addPropertyChangeListener(propertyChangeListener);
317 final IPropertyChangeListener themeListener = event -> {
318 String property = event.getProperty();
319 if (IThemeManager.CHANGE_CURRENT_THEME.equals(property)
320 || UIPreferences.THEME_CommitMessageEditorFont
321 .equals(property)) {
322 Font themeFont = UIUtils
323 .getFont(UIPreferences.THEME_CommitMessageEditorFont);
324 getDisplay().asyncExec(() -> {
325 if (!isDisposed()) {
326 sourceViewer.setFont(themeFont);
331 PlatformUI.getWorkbench().getThemeManager()
332 .addPropertyChangeListener(themeListener);
334 final SourceViewerDecorationSupport support = configureAnnotationPreferences();
336 Document document = new Document(initialText);
338 configuration = new HyperlinkSourceViewer.Configuration(
339 EditorsUI.getPreferenceStore()) {
341 @Override
342 public int getHyperlinkStateMask(ISourceViewer targetViewer) {
343 if (!targetViewer.isEditable()) {
344 return SWT.NONE;
346 return super.getHyperlinkStateMask(targetViewer);
349 @Override
350 protected Map getHyperlinkDetectorTargets(ISourceViewer targetViewer) {
351 return getHyperlinkTargets();
354 @Override
355 public IReconciler getReconciler(ISourceViewer viewer) {
356 if (!isEditable(viewer))
357 return null;
358 return super.getReconciler(sourceViewer);
361 @Override
362 public IContentAssistant getContentAssistant(ISourceViewer viewer) {
363 if (!viewer.isEditable())
364 return null;
365 IContentAssistant assistant = createContentAssistant(viewer);
366 // Add content assist proposal handler if assistant exists
367 if (assistant != null)
368 contentAssistAction = createContentAssistAction(
369 sourceViewer);
370 return assistant;
373 @Override
374 public IPresentationReconciler getPresentationReconciler(
375 ISourceViewer viewer) {
376 PresentationReconciler reconciler = new PresentationReconciler();
377 reconciler.setDocumentPartitioning(
378 getConfiguredDocumentPartitioning(viewer));
379 DefaultDamagerRepairer hyperlinkDamagerRepairer = new DefaultDamagerRepairer(
380 new HyperlinkTokenScanner(this, viewer));
381 reconciler.setDamager(hyperlinkDamagerRepairer,
382 IDocument.DEFAULT_CONTENT_TYPE);
383 reconciler.setRepairer(hyperlinkDamagerRepairer,
384 IDocument.DEFAULT_CONTENT_TYPE);
385 return reconciler;
390 sourceViewer.configure(configuration);
391 sourceViewer.setDocument(document, annotationModel);
393 configureContextMenu();
395 getTextWidget().addDisposeListener(new DisposeListener() {
396 @Override
397 public void widgetDisposed(DisposeEvent disposeEvent) {
398 support.uninstall();
399 Activator.getDefault().getPreferenceStore().removePropertyChangeListener(propertyChangeListener);
400 PlatformUI.getWorkbench().getThemeManager()
401 .removePropertyChangeListener(themeListener);
406 private void computeBrokenBidiPlatformTextWidth(int textWidth) {
407 class BidiSegmentListenerTester implements BidiSegmentListener {
408 boolean called;
410 @Override
411 public void lineGetSegments(BidiSegmentEvent event) {
412 called = true;
415 BidiSegmentListenerTester tester = new BidiSegmentListenerTester();
416 StyledText textWidget = getTextWidget();
417 textWidget.addBidiSegmentListener(tester);
418 textWidget.setText(" "); //$NON-NLS-1$
419 textWidget.computeSize(SWT.DEFAULT, SWT.DEFAULT);
420 textWidget.removeBidiSegmentListener(tester);
422 brokenBidiPlatformTextWidth = tester.called ? -1 : textWidth;
425 private boolean isEditable(ISourceViewer viewer) {
426 return viewer != null && viewer.getTextWidget().getEditable();
429 private void configureHardWrap() {
430 if (shouldHardWrap()) {
431 if (hardWrapSegmentListener == null) {
432 final StyledText textWidget = getTextWidget();
433 hardWrapSegmentListener = new BidiSegmentListener() {
434 @Override
435 public void lineGetSegments(BidiSegmentEvent e) {
436 if (e.widget == textWidget) {
437 int footerOffset = CommonUtils
438 .getFooterOffset(textWidget.getText());
439 if (footerOffset >= 0
440 && e.lineOffset >= footerOffset) {
441 return;
444 int[] segments = calculateWrapOffsets(e.lineText, MAX_LINE_WIDTH);
445 if (segments != null) {
446 char[] segmentsChars = new char[segments.length];
447 Arrays.fill(segmentsChars, '\n');
448 e.segments = segments;
449 e.segmentsChars = segmentsChars;
453 textWidget.addBidiSegmentListener(hardWrapSegmentListener);
454 textWidget.setText(textWidget.getText()); // XXX: workaround for https://bugs.eclipse.org/384886
456 if (brokenBidiPlatformTextWidth != -1) {
457 Layout restrictedWidthLayout = new Layout() {
458 @Override
459 protected Point computeSize(Composite composite,
460 int wHint, int hHint, boolean flushCache) {
461 Point size = SpellcheckableMessageArea.this
462 .getSize();
463 Rectangle trim = SpellcheckableMessageArea.this
464 .computeTrim(0, 0, 0, 0);
465 size.x -= trim.width;
466 size.y -= trim.height;
467 if (size.x > brokenBidiPlatformTextWidth)
468 size.x = brokenBidiPlatformTextWidth;
469 return size;
472 @Override
473 protected void layout(Composite composite,
474 boolean flushCache) {
475 Point size = computeSize(composite, SWT.DEFAULT,
476 SWT.DEFAULT, flushCache);
477 textWidget.setBounds(0, 0, size.x, size.y);
480 setLayout(restrictedWidthLayout);
484 } else if (hardWrapSegmentListener != null) {
485 StyledText textWidget = getTextWidget();
486 textWidget.removeBidiSegmentListener(hardWrapSegmentListener);
487 textWidget.setText(textWidget.getText()); // XXX: workaround for https://bugs.eclipse.org/384886
488 hardWrapSegmentListener = null;
490 if (brokenBidiPlatformTextWidth != -1)
491 setLayout(new FillLayout());
495 private TextViewerAction createFromActionFactory(ActionFactory factory,
496 int operationCode) {
497 IWorkbenchAction template = factory
498 .create(PlatformUI.getWorkbench().getActiveWorkbenchWindow());
499 TextViewerAction action = new TextViewerAction(sourceViewer,
500 operationCode);
501 action.setText(template.getText());
502 action.setImageDescriptor(template.getImageDescriptor());
503 action.setDisabledImageDescriptor(
504 template.getDisabledImageDescriptor());
505 action.setActionDefinitionId(template.getActionDefinitionId());
506 template.dispose();
507 return action;
510 private void configureContextMenu() {
511 final boolean editable = isEditable(sourceViewer);
512 TextViewerAction cutAction;
513 TextViewerAction undoAction;
514 TextViewerAction redoAction;
515 TextViewerAction pasteAction;
516 IAction quickFixAction;
517 if (editable) {
518 cutAction = createFromActionFactory(ActionFactory.CUT,
519 ITextOperationTarget.CUT);
520 undoAction = createFromActionFactory(ActionFactory.UNDO,
521 ITextOperationTarget.UNDO);
522 redoAction = createFromActionFactory(ActionFactory.REDO,
523 ITextOperationTarget.REDO);
524 pasteAction = createFromActionFactory(ActionFactory.PASTE,
525 ITextOperationTarget.PASTE);
526 quickFixAction = new QuickfixAction(sourceViewer);
527 } else {
528 cutAction = null;
529 undoAction = null;
530 redoAction = null;
531 pasteAction = null;
532 quickFixAction = null;
534 TextViewerAction copyAction = createFromActionFactory(
535 ActionFactory.COPY, ITextOperationTarget.COPY);
536 TextViewerAction selectAllAction = createFromActionFactory(
537 ActionFactory.SELECT_ALL, ITextOperationTarget.SELECT_ALL);
539 final TextEditorPropertyAction showWhitespaceAction = new TextEditorPropertyAction(
540 UIText.SpellcheckableMessageArea_showWhitespace,
541 sourceViewer,
542 AbstractTextEditor.PREFERENCE_SHOW_WHITESPACE_CHARACTERS) {
544 private IPainter whitespaceCharPainter;
546 @Override
547 public void propertyChange(PropertyChangeEvent event) {
548 String property = event.getProperty();
549 if (property.equals(getPreferenceKey())
550 || AbstractTextEditor.PREFERENCE_SHOW_LEADING_SPACES
551 .equals(property)
552 || AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_SPACES
553 .equals(property)
554 || AbstractTextEditor.PREFERENCE_SHOW_TRAILING_SPACES
555 .equals(property)
556 || AbstractTextEditor.PREFERENCE_SHOW_LEADING_IDEOGRAPHIC_SPACES
557 .equals(property)
558 || AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_IDEOGRAPHIC_SPACES
559 .equals(property)
560 || AbstractTextEditor.PREFERENCE_SHOW_TRAILING_IDEOGRAPHIC_SPACES
561 .equals(property)
562 || AbstractTextEditor.PREFERENCE_SHOW_LEADING_TABS
563 .equals(property)
564 || AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_TABS
565 .equals(property)
566 || AbstractTextEditor.PREFERENCE_SHOW_TRAILING_TABS
567 .equals(property)
568 || AbstractTextEditor.PREFERENCE_SHOW_CARRIAGE_RETURN
569 .equals(property)
570 || AbstractTextEditor.PREFERENCE_SHOW_LINE_FEED
571 .equals(property)
572 || AbstractTextEditor.PREFERENCE_WHITESPACE_CHARACTER_ALPHA_VALUE
573 .equals(property)) {
574 synchronizeWithPreference();
578 @Override
579 protected void toggleState(boolean checked) {
580 if (checked)
581 installPainter();
582 else
583 uninstallPainter();
587 * Installs the painter on the viewer.
589 private void installPainter() {
590 Assert.isTrue(whitespaceCharPainter == null);
591 ITextViewer v = getTextViewer();
592 if (v instanceof ITextViewerExtension2) {
593 IPreferenceStore store = getStore();
594 whitespaceCharPainter = new WhitespaceCharacterPainter(
596 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LEADING_SPACES),
597 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_SPACES),
598 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_TRAILING_SPACES),
599 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LEADING_IDEOGRAPHIC_SPACES),
600 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_IDEOGRAPHIC_SPACES),
601 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_TRAILING_IDEOGRAPHIC_SPACES),
602 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LEADING_TABS),
603 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_ENCLOSED_TABS),
604 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_TRAILING_TABS),
605 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_CARRIAGE_RETURN),
606 store.getBoolean(AbstractTextEditor.PREFERENCE_SHOW_LINE_FEED),
607 store.getInt(AbstractTextEditor.PREFERENCE_WHITESPACE_CHARACTER_ALPHA_VALUE));
608 ((ITextViewerExtension2) v).addPainter(whitespaceCharPainter);
613 * Remove the painter from the viewer.
615 private void uninstallPainter() {
616 if (whitespaceCharPainter == null)
617 return;
618 ITextViewer v = getTextViewer();
619 if (v instanceof ITextViewerExtension2)
620 ((ITextViewerExtension2) v)
621 .removePainter(whitespaceCharPainter);
622 whitespaceCharPainter.deactivate(true);
623 whitespaceCharPainter = null;
627 MenuManager contextMenu = new MenuManager();
628 if (cutAction != null) {
629 contextMenu.add(cutAction);
631 contextMenu.add(copyAction);
632 if (pasteAction != null) {
633 contextMenu.add(pasteAction);
635 contextMenu.add(selectAllAction);
636 if (undoAction != null) {
637 contextMenu.add(undoAction);
639 if (redoAction != null) {
640 contextMenu.add(redoAction);
642 contextMenu.add(new Separator());
643 contextMenu.add(showWhitespaceAction);
644 contextMenu.add(new Separator());
646 if (editable) {
647 final SubMenuManager quickFixMenu = new SubMenuManager(contextMenu);
648 quickFixMenu.setVisible(true);
649 quickFixMenu.addMenuListener(new IMenuListener() {
650 @Override
651 public void menuAboutToShow(IMenuManager manager) {
652 quickFixMenu.removeAll();
653 addProposals(quickFixMenu);
658 final StyledText textWidget = getTextWidget();
659 List<IAction> globalActions = new ArrayList<>();
660 if (editable) {
661 globalActions.add(cutAction);
662 globalActions.add(pasteAction);
663 globalActions.add(undoAction);
664 globalActions.add(redoAction);
665 globalActions.add(quickFixAction);
667 globalActions.add(copyAction);
668 globalActions.add(selectAllAction);
669 if (contentAssistAction != null) {
670 globalActions.add(contentAssistAction);
672 ActionUtils.setGlobalActions(textWidget, globalActions,
673 getHandlerService());
675 textWidget.setMenu(contextMenu.createContextMenu(textWidget));
677 sourceViewer.addSelectionChangedListener(new ISelectionChangedListener() {
679 @Override
680 public void selectionChanged(SelectionChangedEvent event) {
681 if (cutAction != null)
682 cutAction.update();
683 copyAction.update();
688 if (editable) {
689 sourceViewer.addTextListener(new ITextListener() {
690 @Override
691 public void textChanged(TextEvent event) {
692 if (undoAction != null)
693 undoAction.update();
694 if (redoAction != null)
695 redoAction.update();
700 textWidget.addDisposeListener(new DisposeListener() {
701 @Override
702 public void widgetDisposed(DisposeEvent disposeEvent) {
703 showWhitespaceAction.dispose();
708 private void addProposals(final SubMenuManager quickFixMenu) {
709 IAnnotationModel sourceModel = sourceViewer.getAnnotationModel();
710 if (sourceModel == null) {
711 return;
713 Iterator annotationIterator = sourceModel.getAnnotationIterator();
714 while (annotationIterator.hasNext()) {
715 Annotation annotation = (Annotation) annotationIterator.next();
716 boolean isDeleted = annotation.isMarkedDeleted();
717 boolean isIncluded = !isDeleted
718 && includes(sourceModel.getPosition(annotation),
719 getTextWidget().getCaretOffset());
720 boolean isFixable = isIncluded && sourceViewer
721 .getQuickAssistAssistant().canFix(annotation);
722 if (isFixable) {
723 IQuickAssistProcessor processor = sourceViewer
724 .getQuickAssistAssistant().getQuickAssistProcessor();
725 IQuickAssistInvocationContext context = sourceViewer
726 .getQuickAssistInvocationContext();
727 ICompletionProposal[] proposals = processor
728 .computeQuickAssistProposals(context);
730 for (ICompletionProposal proposal : proposals) {
731 quickFixMenu.add(createQuickFixAction(proposal));
737 private boolean includes(Position position, int caretOffset) {
738 return position != null && (position.includes(caretOffset)
739 || (position.offset + position.length) == caretOffset);
742 private IAction createQuickFixAction(final ICompletionProposal proposal) {
743 return new Action(proposal.getDisplayString()) {
745 @Override
746 public void run() {
747 proposal.apply(sourceViewer.getDocument());
750 @Override
751 public ImageDescriptor getImageDescriptor() {
752 Image image = proposal.getImage();
753 if (image != null)
754 return ImageDescriptor.createFromImage(image);
755 return null;
761 * Return <code>IHandlerService</code>. The default implementation uses the
762 * workbench window's service locator. Subclasses may override to access the
763 * service by using a local service locator.
765 * @return <code>IHandlerService</code> using the workbench window's service
766 * locator. Can be <code>null</code> if the service could not be
767 * found.
769 protected IHandlerService getHandlerService() {
770 return CommonUtils.getService(PlatformUI.getWorkbench(), IHandlerService.class);
773 private SourceViewerDecorationSupport configureAnnotationPreferences() {
774 ISharedTextColors textColors = EditorsUI.getSharedTextColors();
775 IAnnotationAccess annotationAccess = new DefaultMarkerAnnotationAccess();
776 final SourceViewerDecorationSupport support = new SourceViewerDecorationSupport(
777 sourceViewer, null, annotationAccess, textColors);
779 List annotationPreferences = new MarkerAnnotationPreferences()
780 .getAnnotationPreferences();
781 Iterator e = annotationPreferences.iterator();
782 while (e.hasNext())
783 support.setAnnotationPreference((AnnotationPreference) e.next());
785 support.install(EditorsUI.getPreferenceStore());
786 return support;
790 * Create margin painter and add to source viewer
792 protected void createMarginPainter() {
793 MarginPainter marginPainter = new MarginPainter(sourceViewer);
794 marginPainter.setMarginRulerColumn(MAX_LINE_WIDTH);
795 marginPainter.setMarginRulerColor(Display.getDefault().getSystemColor(
796 SWT.COLOR_GRAY));
797 sourceViewer.addPainter(marginPainter);
800 private int getCharWidth() {
801 GC gc = new GC(getTextWidget());
802 int charWidth = gc.getFontMetrics().getAverageCharWidth();
803 gc.dispose();
804 return charWidth;
807 private int getLineHeight() {
808 return getTextWidget().getLineHeight();
812 * @return if the commit message should be hard-wrapped (preference)
814 private static boolean shouldHardWrap() {
815 return Activator.getDefault().getPreferenceStore()
816 .getBoolean(UIPreferences.COMMIT_DIALOG_HARD_WRAP_MESSAGE);
820 * @return widget
822 public StyledText getTextWidget() {
823 return sourceViewer.getTextWidget();
826 private static class QuickfixAction extends Action {
828 private final ITextOperationTarget textOperationTarget;
830 public QuickfixAction(ITextOperationTarget target) {
831 textOperationTarget = target;
832 setActionDefinitionId(
833 ITextEditorActionDefinitionIds.QUICK_ASSIST);
836 @Override
837 public void run() {
838 if (textOperationTarget.canDoOperation(ISourceViewer.QUICK_ASSIST)) {
839 textOperationTarget.doOperation(ISourceViewer.QUICK_ASSIST);
845 private IAction createContentAssistAction(
846 final SourceViewer viewer) {
847 Action proposalAction = new TextViewerAction(viewer,
848 ISourceViewer.CONTENTASSIST_PROPOSALS);
849 proposalAction
850 .setActionDefinitionId(ITextEditorActionDefinitionIds.CONTENT_ASSIST_PROPOSALS);
851 return proposalAction;
855 * Returns the commit message, converting platform-specific line endings to
856 * '\n' and hard-wrapping lines if necessary.
858 * @return commit message, without trailing whitespace on lines and without
859 * trailing empty lines.
861 public String getCommitMessage() {
862 String text = getText();
863 text = Utils.normalizeLineEndings(text);
864 if (shouldHardWrap()) {
865 text = wrapCommitMessage(text);
867 text = TRAILING_WHITE_SPACE_ON_LINES.matcher(text).replaceAll(""); //$NON-NLS-1$
868 text = TRAILING_NEWLINES.matcher(text).replaceFirst("\n"); //$NON-NLS-1$
869 return text;
873 * Wraps a commit message, leaving the footer as defined by
874 * {@link CommonUtils#getFooterOffset(String)} unwrapped.
876 * @param text
877 * of the whole commit message, including footer, using '\n' as
878 * line delimiter
879 * @return the wrapped text
881 protected static String wrapCommitMessage(String text) {
882 // protected in order to be easily testable
883 int footerStart = CommonUtils.getFooterOffset(text);
884 if (footerStart < 0) {
885 return hardWrap(text);
886 } else {
887 // Do not wrap footer lines.
888 String footer = text.substring(footerStart);
889 text = hardWrap(text.substring(0, footerStart));
890 return text + footer;
895 * Hard-wraps the given text.
897 * @param text
898 * the text to wrap, must use '\n' as line delimiter
899 * @return the wrapped text
901 protected static String hardWrap(String text) {
902 // protected for testing
903 int[] wrapOffsets = calculateWrapOffsets(text, MAX_LINE_WIDTH);
904 if (wrapOffsets != null) {
905 StringBuilder builder = new StringBuilder(text.length() + wrapOffsets.length);
906 int prev = 0;
907 for (int cur : wrapOffsets) {
908 builder.append(text.substring(prev, cur));
909 for (int j = cur; j > prev && builder.charAt(builder.length() - 1) == ' '; j--)
910 builder.deleteCharAt(builder.length() - 1);
911 builder.append('\n');
912 prev = cur;
914 builder.append(text.substring(prev));
915 return builder.toString();
917 return text;
921 * Get hyperlink targets
923 * @return map of targets
925 protected Map<String, IAdaptable> getHyperlinkTargets() {
926 return Collections.singletonMap(EditorsUI.DEFAULT_TEXT_EDITOR_ID,
927 getDefaultTarget());
931 * Create content assistant
933 * @param viewer
934 * @return content assistant
936 protected IContentAssistant createContentAssistant(ISourceViewer viewer) {
937 return null;
941 * Get default target for hyperlink presenter
943 * @return target
945 protected IAdaptable getDefaultTarget() {
946 return null;
950 * @return text
952 public String getText() {
953 return getDocument().get();
957 * @return document
959 public IDocument getDocument() {
960 return sourceViewer.getDocument();
964 * @param text
966 public void setText(String text) {
967 if (text != null) {
968 getDocument().set(text);
973 * Set the same background color to the styledText widget as the Composite
975 @Override
976 public void setBackground(Color color) {
977 super.setBackground(color);
978 StyledText textWidget = getTextWidget();
979 textWidget.setBackground(color);
985 @Override
986 public boolean forceFocus() {
987 return getTextWidget().setFocus();
991 * Calculates wrap offsets for the given line, so that resulting lines are
992 * no longer than <code>maxLineLength</code> if possible.
994 * @param line
995 * the line to wrap (can contain '\n', but no other line delimiters)
996 * @param maxLineLength
997 * the maximum line length
998 * @return an array of offsets where hard-wraps should be inserted, or
999 * <code>null</code> if the line does not need to be wrapped
1001 public static int[] calculateWrapOffsets(final String line, final int maxLineLength) {
1002 if (line.length() == 0)
1003 return null;
1005 IntList wrapOffsets = new IntList();
1006 int wordStart = 0;
1007 int lineStart = 0;
1008 int length = line.length();
1009 int nofPreviousWordChars = 0;
1010 int nofCurrentWordChars = 0;
1011 for (int i = 0; i < length; i++) {
1012 char ch = line.charAt(i);
1013 if (ch == ' ') {
1014 nofPreviousWordChars += nofCurrentWordChars;
1015 nofCurrentWordChars = 0;
1016 } else if (ch == '\n') {
1017 lineStart = i + 1;
1018 wordStart = i + 1;
1019 nofPreviousWordChars = 0;
1020 nofCurrentWordChars = 0;
1021 } else { // a word character
1022 if (nofCurrentWordChars == 0) {
1023 // Break only if we had a certain number of previous
1024 // non-space characters. If we had less, break only if we
1025 // had at least one non-space character and the current line
1026 // offset is at least half the maximum width. This prevents
1027 // breaking if we have <blanks><very_long_word>, and also
1028 // for things like "[1] <very_long_word>" or mark-up lists
1029 // such as " * <very_long_word>".
1030 if (nofPreviousWordChars > maxLineLength / 10
1031 || nofPreviousWordChars > 0
1032 && (i - lineStart) > maxLineLength / 2) {
1033 wordStart = i;
1036 nofCurrentWordChars++;
1037 if (i >= lineStart + maxLineLength) {
1038 if (wordStart != lineStart) { // don't break before a single long word
1039 wrapOffsets.add(wordStart);
1040 lineStart = wordStart;
1041 nofPreviousWordChars = 0;
1042 nofCurrentWordChars = 0;
1048 int size = wrapOffsets.size();
1049 if (size == 0) {
1050 return null;
1051 } else {
1052 int[] result = new int[size];
1053 for (int i = 0; i < size; i++) {
1054 result[i] = wrapOffsets.get(i);
1056 return result;