diff options
Diffstat (limited to 'org.eclipse.ui.editors/src/org/eclipse/ui/internal')
9 files changed, 934 insertions, 0 deletions
diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/EditorsPluginPreferenceInitializer.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/EditorsPluginPreferenceInitializer.java index 0e5eed62ea4..2c16f3afee4 100644 --- a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/EditorsPluginPreferenceInitializer.java +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/EditorsPluginPreferenceInitializer.java @@ -22,6 +22,7 @@ import org.eclipse.jface.preference.PreferenceConverter; import org.eclipse.jface.resource.ColorRegistry; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.editors.text.codemining.annotation.AnnotationCodeMiningPreferenceConstants; import org.eclipse.ui.internal.texteditor.ITextEditorThemeConstants; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; @@ -42,6 +43,7 @@ public class EditorsPluginPreferenceInitializer extends AbstractPreferenceInitia IPreferenceStore store= EditorsPlugin.getDefault().getPreferenceStore(); TextEditorPreferenceConstants.initializeDefaultValues(store); migrateOverviewRulerPreference(store); + AnnotationCodeMiningPreferenceConstants.initializeDefaultValues(store); } public static void setThemeBasedPreferences(IPreferenceStore store, boolean fireEvent) { diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java index 47c22c621f8..49be4ce042b 100644 --- a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java @@ -74,6 +74,7 @@ import org.eclipse.ui.PlatformUI; import org.eclipse.ui.dialogs.PreferencesUtil; import org.eclipse.ui.internal.editors.text.OverlayPreferenceStore.OverlayKey; import org.eclipse.ui.internal.editors.text.TextEditorDefaultsPreferencePage.EnumeratedDomain.EnumValue; +import org.eclipse.ui.internal.editors.text.codemining.annotation.AnnotationCodeMiningPreferenceConstants; import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.eclipse.ui.texteditor.AbstractTextEditor; @@ -771,6 +772,9 @@ public class TextEditorDefaultsPreferencePage extends PreferencePage implements overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_TEXT_HOVER_AFFORDANCE)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_HOVER_ENRICH_MODE)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_MAX)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_LEADING_SPACES)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_ENCLOSED_SPACES)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SHOW_TRAILING_SPACES)); @@ -941,6 +945,39 @@ public class TextEditorDefaultsPreferencePage extends PreferencePage implements Preference smartHomeEnd= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SMART_HOME_END, label, null); addCheckBox(appearanceComposite, smartHomeEnd, new BooleanDomain(), 0); + label= TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_show; + String description= TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_description; + Preference showCodeMinings= new Preference(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL, label, description); + EnumeratedDomain codeMiningsDomain= new EnumeratedDomain(); + codeMiningsDomain.addValue(new EnumValue(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__NONE, TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_none)); + codeMiningsDomain.addValue(new EnumValue(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR, TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_error)); + codeMiningsDomain.addValue(new EnumValue(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR_WARNING, + TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_ErrorWarnings)); + codeMiningsDomain.addValue(new EnumValue(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR_WARNING_INFO, + TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_ErrowWarningsInfo)); + final Control[] showCodeMiningsControls= addCombo(appearanceComposite, showCodeMinings, codeMiningsDomain, 0); + + label= TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_max; + description= TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_max_description; + Preference maxCodeMinings= new Preference(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_MAX, label, description); + IntegerDomain maxCodeMiningsDomain= new IntegerDomain(0, 99999); + Control[] maxCodeMiningsControls= addTextField(appearanceComposite, maxCodeMinings, maxCodeMiningsDomain, 15, 20); + + final SelectionListener codeMiningsListener= new SelectionAdapter() { + @Override + public void widgetSelected(SelectionEvent e) { + final int showCodeMiningsSetting= fOverlayStore.getInt(showCodeMinings.getKey()); + boolean enabled= showCodeMiningsSetting != AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__NONE; + for (Control control : maxCodeMiningsControls) { + control.setEnabled(enabled); + } + } + }; + + ((Combo) showCodeMiningsControls[1]).addSelectionListener(codeMiningsListener); + fMasterSlaveListeners.add(codeMiningsListener); + + addFiller(appearanceComposite, 2); Label l= new Label(appearanceComposite, SWT.LEFT); diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java index 93e6ea4c5d4..2ff3e9ed0be 100644 --- a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java @@ -137,6 +137,14 @@ final class TextEditorMessages extends NLS { public static String TextEditorDefaultsPreferencePage_carriageReturn; public static String TextEditorDefaultsPreferencePage_transparencyLevel; + public static String TextEditorDefaultsPreferencePage_codeMinings_description; + public static String TextEditorDefaultsPreferencePage_codeMinings_error; + public static String TextEditorDefaultsPreferencePage_codeMinings_ErrorWarnings; + public static String TextEditorDefaultsPreferencePage_codeMinings_ErrowWarningsInfo; + public static String TextEditorDefaultsPreferencePage_codeMinings_max; + public static String TextEditorDefaultsPreferencePage_codeMinings_max_description; + public static String TextEditorDefaultsPreferencePage_codeMinings_none; + public static String TextEditorDefaultsPreferencePage_codeMinings_show; public static String TextEditorDefaultsPreferencePage_configureWhitespaceCharacterPainterProperties; public static String TextEditorDefaultsPreferencePage_deleteSpacesAsTabs; public static String TextEditorDefaultsPreferencePage_enclosed; diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties index a316d243636..c41c9eb9b14 100644 --- a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties @@ -39,6 +39,14 @@ TextEditorPreferencePage_accessibility_wideCaret= &Enable thick caret TextEditorPreferencePage_accessibility_useSaturatedColorsInOverviewRuler=U&se saturated colors in overview ruler TextEditorDefaultsPreferencePage_carriageReturn=Carriage Return ( \u00a4 ) TextEditorDefaultsPreferencePage_transparencyLevel=&Transparency level (0 is transparent and 255 is opaque): +TextEditorDefaultsPreferencePage_codeMinings_description=How annotations should be shown in-line in text editors which support Code Minings +TextEditorDefaultsPreferencePage_codeMinings_error=Error +TextEditorDefaultsPreferencePage_codeMinings_ErrorWarnings=Error / Warnings +TextEditorDefaultsPreferencePage_codeMinings_ErrowWarningsInfo=Error / Warnings / Info +TextEditorDefaultsPreferencePage_codeMinings_max=Maximum annotations shown: +TextEditorDefaultsPreferencePage_codeMinings_max_description=Limits the number of shown annotations to prevent performance issues +TextEditorDefaultsPreferencePage_codeMinings_none=None +TextEditorDefaultsPreferencePage_codeMinings_show=Show Code Minings for Annotations: TextEditorDefaultsPreferencePage_configureWhitespaceCharacterPainterProperties=Configure visibility of whitespace characters in different regions of a line of text: TextEditorDefaultsPreferencePage_deleteSpacesAsTabs=Remove &multiple spaces on backspace/delete TextEditorDefaultsPreferencePage_enclosed=Enclosed diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMining.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMining.java new file mode 100644 index 00000000000..2453a30a111 --- /dev/null +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMining.java @@ -0,0 +1,65 @@ +/******************************************************************************* + * Copyright (c) 2018 Altran Netherlands B.V. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Niko Stotz (Altran Netherlands B.V.) - initial implementation + *******************************************************************************/ +package org.eclipse.ui.internal.editors.text.codemining.annotation; + +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.events.MouseEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.GC; +import org.eclipse.swt.graphics.Point; +import org.eclipse.swt.graphics.Rectangle; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.codemining.ICodeMiningProvider; +import org.eclipse.jface.text.codemining.LineHeaderCodeMining; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationAccessExtension; + +/** + * Draws an Annotation's text and icon as Line header code mining. + * + * @since 3.13 + */ +@NonNullByDefault +public class AnnotationCodeMining extends LineHeaderCodeMining { + private final Annotation annotation; + + private final IAnnotationAccessExtension annotationAccess; + + public AnnotationCodeMining(IAnnotationAccessExtension annotationAccess, Annotation annotation, int lineNumber, IDocument document, ICodeMiningProvider provider, + @Nullable Consumer<MouseEvent> action) throws BadLocationException { + super(lineNumber, document, provider, action); + this.annotationAccess= annotationAccess; + + setLabel(annotation.getText()); + + this.annotation= annotation; + } + + @Override + @SuppressWarnings("null") + public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { + final int width= 16; + annotationAccess.paint(this.annotation, gc, textWidget, new Rectangle(x, y, width, 16)); + final Point result= super.draw(gc, textWidget, color, x + width, y); + result.x+= width; + return result; + } +} diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningFilter.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningFilter.java new file mode 100644 index 00000000000..f855ba8a2ed --- /dev/null +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningFilter.java @@ -0,0 +1,239 @@ +/******************************************************************************* + * Copyright (c) 2019 Altran Netherlands B.V. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Niko Stotz (Altran Netherlands B.V.) - initial implementation + *******************************************************************************/ +package org.eclipse.ui.internal.editors.text.codemining.annotation; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import org.eclipse.core.resources.IMarker; + +import org.eclipse.jface.text.quickassist.IQuickFixableAnnotation; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.IAnnotationAccessExtension; +import org.eclipse.jface.text.source.IAnnotationPresentation; + +import org.eclipse.ui.texteditor.MarkerAnnotation; + +/** + * Filters and arranges Annotations that are suitable as code minings. Takes user preferences into + * account. + * + * @since 3.13 + */ +@NonNullByDefault +public class AnnotationCodeMiningFilter { + /** + * Callback to locate an Annotation inside the editor. + */ + public interface Locator { + public @Nullable Integer getOffset(Annotation annotation); + + public @Nullable Integer getLine(Annotation annotation); + } + + final private IAnnotationAccessExtension annotationAccess; + + final private AnnotationCodeMiningPreferences preferences= new AnnotationCodeMiningPreferences(); + + final private Stream<Annotation> annotations; + + public AnnotationCodeMiningFilter(IAnnotationAccessExtension annotationAccess, Annotation[]... annotations) { + this.annotationAccess= annotationAccess; + this.annotations= Arrays.stream(annotations).flatMap(Arrays::stream); + } + + public AnnotationCodeMiningFilter(IAnnotationAccessExtension annotationAccess, Iterator<Annotation> annotations) { + this.annotationAccess= annotationAccess; + this.annotations= StreamSupport.stream(Spliterators.spliteratorUnknownSize(annotations, Spliterator.ORDERED), false); + } + + /** + * Checks if there are any suitable annotations. + */ + public boolean isEmpty() { + return !filterReTrigger(annotations).findAny().isPresent(); + } + + /** + * Returns all suitable annotations. + */ + public Stream<Annotation> sortDistinctLimit(Locator locator) { + return limit(distinct(locator, sort(locator, filterShown(filterReTrigger(annotations))))); + } + + /** + * Filters suitable annotations to decide whether we need to re-trigger code minings. + */ + private Stream<Annotation> filterReTrigger(Stream<Annotation> anns) { + return anns + .filter(this::isTypeProcessable) + .filter(this::isPaintable) + .filter(this::isInScope); + } + + private boolean isTypeProcessable(Annotation a) { + return a instanceof MarkerAnnotation || + a instanceof IQuickFixableAnnotation || + a instanceof IAnnotationPresentation; + } + + private boolean isPaintable(Annotation a) { + // workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=552760 + // (NPE on JavaAnnotationImageProvider.getQuickFixErrorImage(): No Display) + try { + return annotationAccess.isPaintable(a); + } catch (NullPointerException e) { + return false; + } + } + + private boolean isInScope(Annotation a) { + return isError(a) || isWarning(a) || isInfo(a); + } + + /** + * Filters suitable annotations to show. + */ + private Stream<Annotation> filterShown(Stream<Annotation> anns) { + return anns + .filter(a -> !a.isMarkedDeleted()) + .filter(this::isEnabled); + } + + private boolean isEnabled(Annotation a) { + if (isError(a)) { + return preferences.isErrorEnabled(); + } else if (isWarning(a)) { + return preferences.isWarningEnabled(); + } else if (isInfo(a)) { + return preferences.isInfoEnabled(); + } else { + return false; + } + } + + /** + * Sorts annotations based on 1) position in text, 2) layer, 3) severity, 4) text. + */ + private Stream<Annotation> sort(Locator locator, Stream<Annotation> anns) { + return anns.sorted((a, b) -> { + int resultPosition= comparePosition(locator, a, b); + if (resultPosition != 0) { + return resultPosition; + } + + final int resultLayer= compareLayer(a, b); + if (resultLayer != 0) { + return resultLayer; + } + + final int resultSeverity= compareSeverity(a, b); + if (resultSeverity != 0) { + return resultSeverity; + } + + return a.getText().compareTo(b.getText()); + }); + } + + private int comparePosition(Locator locator, Annotation a, Annotation b) { + final Integer aOffset= locator.getOffset(a); + final Integer bOffset= locator.getOffset(b); + + if (aOffset == null || bOffset == null) { + return 0; + } + + int resultPosition= Integer.compare(aOffset, bOffset); + return resultPosition; + } + + private int compareLayer(Annotation a, Annotation b) { + final int resultPriority= Integer.compare(annotationAccess.getLayer(a), annotationAccess.getLayer(b)); + return resultPriority; + } + + private int compareSeverity(Annotation a, Annotation b) { + final int resultSeverity= Integer.compare(getSeverity(a), getSeverity(b)); + return resultSeverity; + } + + private int getSeverity(Annotation a) { + if (isError(a)) { + return IMarker.SEVERITY_ERROR; + } else if (isWarning(a)) { + return IMarker.SEVERITY_WARNING; + } else if (isInfo(a)) { + return IMarker.SEVERITY_INFO; + } else { + return -1; + } + } + + /** + * Assures annotations are distinct by line and text. Required, as sometimes the same message is + * shown at the same place from different annotations. + */ + @SuppressWarnings("null") + private Stream<Annotation> distinct(Locator locator, Stream<Annotation> anns) { + return anns + .filter(distinctByKey(a -> { + final Integer line= locator.getLine(a); + if (line == null) { + return null; + } + String key= line + a.getText(); + + return key; + })) + .filter(Objects::nonNull); + } + + /** + * Limits annotations to user-defined amount. + */ + private Stream<Annotation> limit(Stream<Annotation> anns) { + return anns.limit(preferences.getMaxMinings()); + } + + private boolean isInfo(Annotation a) { + return annotationAccess.isSubtype(a.getType(), "org.eclipse.ui.workbench.texteditor.info"); //$NON-NLS-1$ + } + + private boolean isWarning(Annotation a) { + return annotationAccess.isSubtype(a.getType(), "org.eclipse.ui.workbench.texteditor.warning"); //$NON-NLS-1$ + } + + private boolean isError(Annotation a) { + return annotationAccess.isSubtype(a.getType(), "org.eclipse.ui.workbench.texteditor.error"); //$NON-NLS-1$ + } + + @SuppressWarnings("null") + public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) { + Map<Object, Boolean> seen= new LinkedHashMap<>(); + return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null; + } +} diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningPreferenceConstants.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningPreferenceConstants.java new file mode 100644 index 00000000000..2ac45d87007 --- /dev/null +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningPreferenceConstants.java @@ -0,0 +1,136 @@ +/******************************************************************************* + * Copyright (c) 2019 Altran Netherlands B.V. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Niko Stotz (Altran Netherlands B.V.) - initial implementation + *******************************************************************************/ +package org.eclipse.ui.internal.editors.text.codemining.annotation; + +import org.eclipse.jface.preference.IPreferenceStore; + +import org.eclipse.ui.internal.editors.text.EditorsPlugin; + +/** + * Preference constants used for the annotation code mining preference store. + * + * @since 3.13 + * @noinstantiate This class is not intended to be instantiated by clients. + * @noextend This class is not intended to be subclassed by clients. + */ +public class AnnotationCodeMiningPreferenceConstants { + private AnnotationCodeMiningPreferenceConstants() { + + } + + /** + * A named preference that controls which {@link org.eclipse.jface.text.source.Annotation + * Annotations} level should be shown as code minings. + * <p> + * Value is of type <code>Integer</code>. + * </p> + * + * @since 3.13 + */ + public final static String SHOW_ANNOTATION_CODE_MINING_LEVEL= "showAnnotationAsCodeMiningLevel"; //$NON-NLS-1$ + + /** + * Value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL} to show no annotation code minings. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__NONE= 0b0; + + /** + * Value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL} to show info annotation code minings. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__INFO= 0b10; + + /** + * Value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL} to show warning annotation code minings. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__WARNING= 0b100; + + /** + * Value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL} to show error annotation code minings. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR= 0b1000; + + /** + * Value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL} to show error and warning annotation + * code minings. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR_WARNING= SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR + | SHOW_ANNOTATION_CODE_MINING_LEVEL__WARNING; + + /** + * Value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL} to show error, warning, and info + * annotation code minings. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR_WARNING_INFO= SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR + | SHOW_ANNOTATION_CODE_MINING_LEVEL__WARNING + | SHOW_ANNOTATION_CODE_MINING_LEVEL__INFO; + + /** + * Default value for {@link #SHOW_ANNOTATION_CODE_MINING_LEVEL}. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_LEVEL__DEFAULT= SHOW_ANNOTATION_CODE_MINING_LEVEL__NONE; + + /** + * A named preference that controls how many {@link org.eclipse.jface.text.source.Annotation + * Annotations}s should be shown at most as code minings. + * <p> + * Value is of type <code>Integer</code>. + * </p> + * + * @since 3.13 + */ + public final static String SHOW_ANNOTATION_CODE_MINING_MAX= "showAnnotationAsCodeMiningMax"; //$NON-NLS-1$ + + /** + * Default value for {@link #SHOW_ANNOTATION_CODE_MINING_MAX}. + * + * @since 3.13 + */ + public final static int SHOW_ANNOTATION_CODE_MINING_MAX__DEFAULT= 100; + + /** + * Returns the Generic Editor preference store. + * + * @return the Generic Editor preference store + */ + public static IPreferenceStore getPreferenceStore() { + return EditorsPlugin.getDefault().getPreferenceStore(); + } + + /** + * Initializes the given preference store with the default values. + * + * @param store the preference store to be initialized + * + * @since 3.13 + */ + public static void initializeDefaultValues(IPreferenceStore store) { + store.setDefault(SHOW_ANNOTATION_CODE_MINING_LEVEL, SHOW_ANNOTATION_CODE_MINING_LEVEL__DEFAULT); + store.setDefault(SHOW_ANNOTATION_CODE_MINING_MAX, SHOW_ANNOTATION_CODE_MINING_MAX__DEFAULT); + } + +} diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningPreferences.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningPreferences.java new file mode 100644 index 00000000000..fc8a2f779bd --- /dev/null +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningPreferences.java @@ -0,0 +1,97 @@ +/******************************************************************************* + * Copyright (c) 2019 Altran Netherlands B.V. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Niko Stotz (Altran Netherlands B.V.) - initial implementation + *******************************************************************************/ +package org.eclipse.ui.internal.editors.text.codemining.annotation; + +import org.eclipse.jdt.annotation.Nullable; + +import org.eclipse.jface.preference.IPreferenceStore; + +/** + * Simplifies access to user preferences related to Annotation-based code minings. + * + * <p> + * All methods fall back to defaults if the preference store is unavailable. + * </p> + * + * <p> + * The following preferences are available: + * </p> + * + * <dl> + * <dt>show infos <i>(default: <tt>false</tt>)</i></dt> + * <dd>Whether INFO-level annotations should be shown as code minings.</dd> + * + * <dt>show warnings <i>(default: <tt>false</tt>)</i></dt> + * <dd>Whether WARNING-level annotations should be shown as code minings.</dd> + * + * <dt>show errors <i>(default: <tt>false</tt>)</i></dt> + * <dd>Whether ERROR-level annotations should be shown as code minings.</dd> + * + * <dt>Maximum annotations shown <i>(default: <tt>100</tt>)</i></dt> + * <dd>How many annotations should be shown at most as code minings. Mainly to prevent bad editor + * performance.</dd> + * </dl> + * + * @since 3.13 + */ +public class AnnotationCodeMiningPreferences { + private IPreferenceStore preferenceStore; + + public boolean isInfoEnabled() { + return (getLevel() & AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__INFO) > 0; + } + + public boolean isWarningEnabled() { + return (getLevel() & AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__WARNING) > 0; + } + + public boolean isErrorEnabled() { + return (getLevel() & AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR) > 0; + } + + public boolean isEnabled() { + final IPreferenceStore node= getPreferences(); + if (node == null) { + return false; + } + + final int max= getMaxMinings(); + + return max > 0 && getLevel() > 0; + } + + int getLevel() { + IPreferenceStore node= getPreferences(); + + return node != null + ? node.getInt(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL) + : AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__DEFAULT; + } + + int getMaxMinings() { + final IPreferenceStore node= getPreferences(); + + return node != null + ? node.getInt(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_MAX) + : AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_MAX__DEFAULT; + } + + protected @Nullable IPreferenceStore getPreferences() { + if (preferenceStore == null) { + preferenceStore= AnnotationCodeMiningPreferenceConstants.getPreferenceStore(); + } + + return preferenceStore; + } +} diff --git a/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningProvider.java b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningProvider.java new file mode 100644 index 00000000000..ed6be29f231 --- /dev/null +++ b/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/codemining/annotation/AnnotationCodeMiningProvider.java @@ -0,0 +1,342 @@ +/******************************************************************************* + * Copyright (c) 2018 Altran Netherlands B.V. and others. + * + * This program and the accompanying materials + * are made available under the terms of the Eclipse Public License 2.0 + * which accompanies this distribution, and is available at + * https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + * + * Contributors: + * Niko Stotz (Altran Netherlands B.V.) - initial implementation + *******************************************************************************/ +package org.eclipse.ui.internal.editors.text.codemining.annotation; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +import org.eclipse.swt.events.MouseEvent; + +import org.eclipse.core.runtime.Assert; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.core.runtime.Status; + +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.util.IPropertyChangeListener; +import org.eclipse.jface.util.PropertyChangeEvent; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.codemining.AbstractCodeMining; +import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider; +import org.eclipse.jface.text.codemining.ICodeMining; +import org.eclipse.jface.text.quickassist.IQuickAssistAssistant; +import org.eclipse.jface.text.quickassist.IQuickFixableAnnotation; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.AnnotationModelEvent; +import org.eclipse.jface.text.source.IAnnotationAccess; +import org.eclipse.jface.text.source.IAnnotationAccessExtension; +import org.eclipse.jface.text.source.IAnnotationModel; +import org.eclipse.jface.text.source.IAnnotationModelListener; +import org.eclipse.jface.text.source.IAnnotationModelListenerExtension; +import org.eclipse.jface.text.source.ISourceViewerExtension2; +import org.eclipse.jface.text.source.ISourceViewerExtension3; +import org.eclipse.jface.text.source.ISourceViewerExtension5; + +import org.eclipse.ui.internal.editors.text.EditorsPlugin; + +import org.eclipse.ui.editors.text.EditorsUI; + +/** + * Shows <i>info</i>, <i>warning</i>, and <i>error</i> Annotations as line header code minings. + * + * <p> + * If the annotation is quickfixable, clicking on the code mining triggers the quickfix. + * </p> + * <p> + * The user can configure which and how many Annotations should be shown in preferences. + * </p> + * <p> + * Works out-of-the-box for all code mining-enabled text editors. + * </p> + * + * @since 3.13 + * @see org.eclipse.ui.internal.editors.text.codemining.annotation.AnnotationCodeMiningPreferences + */ +@NonNullByDefault +public class AnnotationCodeMiningProvider extends AbstractCodeMiningProvider + implements AnnotationCodeMiningFilter.Locator { + + /** + * Updates code minings after changes to preferences. + */ + private class PropertyChangeListener implements IPropertyChangeListener { + + @Override + public void propertyChange(PropertyChangeEvent event) { + switch (event.getProperty()) { + case AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL: + case AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_MAX: + getCodeMiningViewer().updateCodeMinings(); + break; + default: + // ignore + } + } + } + + /** + * Updates code minings after changes to annotations. + */ + private class AnnotationModelListener implements IAnnotationModelListener, IAnnotationModelListenerExtension { + @Override + public void modelChanged(@Nullable IAnnotationModel model) { + // ignore + } + + @Override + public void modelChanged(@SuppressWarnings("null") AnnotationModelEvent event) { + if (viewer == null) { + return; + } + + if (!event.isValid() || event.isEmpty()) { + return; + } + + AnnotationCodeMiningFilter filter= new AnnotationCodeMiningFilter(getAnnotationAccess(), + event.getAddedAnnotations(), event.getRemovedAnnotations(), event.getChangedAnnotations()); + + if (!filter.isEmpty()) { + getCodeMiningViewer().updateCodeMinings(); + } + } + } + + private @Nullable ITextViewer viewer= null; + + private @Nullable AnnotationModelListener annotationModelListener= null; + + private @Nullable PropertyChangeListener propertyChangeListener= null; + + private @Nullable IAnnotationAccessExtension annotationAccess= null; + + @Override + @SuppressWarnings("null") + public CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer, IProgressMonitor monitor) { + if (!(viewer instanceof ISourceViewerExtension5)) { + throw new IllegalArgumentException("Cannot attach to TextViewer without code mining support"); //$NON-NLS-1$ + } + + if (!new AnnotationCodeMiningPreferences().isEnabled()) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + + this.viewer= viewer; + + final IAnnotationAccess annotationAccess= getAdapter(IAnnotationAccess.class); + if (!(annotationAccess instanceof IAnnotationAccessExtension)) { + throw new IllegalStateException("annotationAccess must implement IAnnotationAccessExtension"); //$NON-NLS-1$ + } + this.annotationAccess= (IAnnotationAccessExtension) annotationAccess; + + return provideCodeMiningsInternal(monitor); + } + + @Override + public void dispose() { + unregisterPropertyChangeListener(); + unregisterAnnotationModelListener(); + + this.viewer= null; + + super.dispose(); + } + + @Override + @SuppressWarnings("boxing") + public @Nullable Integer getLine(Annotation annotation) { + Integer offset= getOffset(annotation); + if (offset == null) { + return null; + } + + try { + return getDocument().getLineOfOffset(offset); + } catch (BadLocationException e) { + return null; + } + } + + @Override + @SuppressWarnings("boxing") + public @Nullable Integer getOffset(Annotation annotation) { + final Position position= getAnnotationModel().getPosition(annotation); + if (position == null) { + return null; + } + + return position.getOffset(); + } + + private CompletableFuture<List<? extends ICodeMining>> provideCodeMiningsInternal(IProgressMonitor monitor) { + return CompletableFuture.supplyAsync(() -> { + if (!checkAnnotationModelAvailable()) { + return Collections.emptyList(); + } + + registerAnnotationModelListener(); + registerPropertyChangeListener(); + + final Stream<Annotation> annotations= getAnnotations(); + final List<AbstractCodeMining> codeMinings= createCodeMinings(annotations, monitor); + + return codeMinings; + }); + } + + private void registerAnnotationModelListener() { + if (annotationModelListener == null) { + annotationModelListener= new AnnotationModelListener(); + getAnnotationModel().addAnnotationModelListener(annotationModelListener); + } + } + + private void unregisterAnnotationModelListener() { + if (this.annotationModelListener != null) { + getAnnotationModel().removeAnnotationModelListener(annotationModelListener); + this.annotationModelListener= null; + } + } + + private void registerPropertyChangeListener() { + if (propertyChangeListener == null) { + final @Nullable IPreferenceStore store= new AnnotationCodeMiningPreferences().getPreferences(); + if (store != null) { + propertyChangeListener= new PropertyChangeListener(); + store.addPropertyChangeListener(propertyChangeListener); + } + } + } + + private void unregisterPropertyChangeListener() { + if (this.propertyChangeListener != null) { + final @Nullable IPreferenceStore store= new AnnotationCodeMiningPreferences().getPreferences(); + if (store != null) { + store.removePropertyChangeListener(propertyChangeListener); + } + this.propertyChangeListener= null; + } + } + + private Stream<Annotation> getAnnotations() { + return new AnnotationCodeMiningFilter(getAnnotationAccess(), getAnnotationModel().getAnnotationIterator()) + .sortDistinctLimit(this); + } + + private List<AbstractCodeMining> createCodeMinings(Stream<Annotation> annotations, IProgressMonitor monitor) { + @SuppressWarnings("null") + final Stream<AbstractCodeMining> result= annotations + .filter(m -> !monitor.isCanceled()) + .map(this::createCodeMining) + .filter(Objects::nonNull); + return result.collect(Collectors.toList()); + } + + @SuppressWarnings("boxing") + private @Nullable AbstractCodeMining createCodeMining(Annotation annotation) { + final Integer lineNumber= getLine(annotation); + + if (lineNumber == null) { + return null; + } + + try { + final Consumer<MouseEvent> action= createAction(annotation); + return new AnnotationCodeMining(getAnnotationAccess(), annotation, lineNumber, getDocument(), this, action); + } catch (BadLocationException e) { + return null; + } + } + + /** + * The action selects the text attached to the Annotation and activates quickfixes. + */ + private @Nullable Consumer<MouseEvent> createAction(Annotation annotation) { + if (!(annotation instanceof IQuickFixableAnnotation) || !(getTextViewer() instanceof ISourceViewerExtension3)) { + return null; + } + + final Position position= getAnnotationModel().getPosition(annotation); + if (position == null) { + return null; + } + + return (e -> { + final IQuickFixableAnnotation quickFixableAnnotation= (IQuickFixableAnnotation) annotation; + if (!quickFixableAnnotation.isQuickFixableStateSet() || !quickFixableAnnotation.isQuickFixable()) { + return; + } + + final IQuickAssistAssistant quickAssistAssistant= ((ISourceViewerExtension3) getTextViewer()).getQuickAssistAssistant(); + if (quickAssistAssistant == null) { + return; + } + + if (!quickAssistAssistant.canFix(annotation)) { + return; + } + + getTextViewer().setSelectedRange(position.getOffset(), position.getLength()); + + final String message= quickAssistAssistant.showPossibleQuickAssists(); + + if (message != null) { + EditorsPlugin.getDefault().getLog().log(new Status(IStatus.ERROR, EditorsUI.PLUGIN_ID, message)); + } + + }); + } + + private boolean checkAnnotationModelAvailable() { + return viewer != null && getAnnotationViewer().getVisualAnnotationModel() != null; + } + + private IAnnotationModel getAnnotationModel() { + return getAnnotationViewer().getVisualAnnotationModel(); + } + + private ITextViewer getTextViewer() { + Assert.isNotNull(viewer); + return viewer; + } + + private ISourceViewerExtension5 getCodeMiningViewer() { + return (ISourceViewerExtension5) getTextViewer(); + } + + private ISourceViewerExtension2 getAnnotationViewer() { + return (ISourceViewerExtension2) getTextViewer(); + } + + private IDocument getDocument() { + return getTextViewer().getDocument(); + } + + private IAnnotationAccessExtension getAnnotationAccess() { + Assert.isNotNull(annotationAccess); + return annotationAccess; + } +} |