diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java index 29fc26f8684..a6b8c208801 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorDefaultsPreferencePage.java @@ -38,6 +38,7 @@ import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Shell; @@ -79,6 +80,8 @@ import org.eclipse.ui.internal.editors.text.TextEditorDefaultsPreferencePage.EnumeratedDomain.EnumValue; import org.eclipse.ui.internal.editors.text.codemining.annotation.AnnotationCodeMiningPreferenceConstants; +import org.eclipse.ui.internal.texteditor.BlockEndCodeMiningPreferenceConstants; + import org.eclipse.ui.texteditor.AbstractDecoratedTextEditorPreferenceConstants; import org.eclipse.ui.texteditor.AbstractTextEditor; @@ -808,6 +811,9 @@ private OverlayPreferenceStore createOverlayStore() { 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, BlockEndCodeMiningPreferenceConstants.SHOW_BLOCK_END_CODE_MINING)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, BlockEndCodeMiningPreferenceConstants.BLOCK_END_CODE_MINING_MIN_LINES)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_ENABLED)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.INT, AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_MAXIMUM_COUNT)); @@ -995,8 +1001,26 @@ public void widgetSelected(SelectionEvent e) { Preference smartHomeEnd= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_SMART_HOME_END, label, null); addCheckBox(appearanceComposite, smartHomeEnd, new BooleanDomain(), 0); + label= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingEnabled; + Preference stickyScrollingEnabled= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_ENABLED, label, null); + Button stickyScrollingEnabledButton= addCheckBox(appearanceComposite, stickyScrollingEnabled, new BooleanDomain(), 0); + + label= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount; + String description= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount; + Preference stickyScrollingMaximumCount= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_MAXIMUM_COUNT, label, description); + final IntegerDomain stickyScrollingMaximumCountDomain= new IntegerDomain(1, 10); + final Control[] stickyScrollingMaximumCountControls= addTextField(appearanceComposite, stickyScrollingMaximumCount, stickyScrollingMaximumCountDomain, 15, 20); + createDependency(stickyScrollingEnabledButton, stickyScrollingEnabled, stickyScrollingMaximumCountControls); + + Group codeMiningGroup= new Group(appearanceComposite, SWT.NONE); + codeMiningGroup.setText(TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_group); + codeMiningGroup.setLayout(new GridLayout(2, false)); + gd= new GridData(GridData.FILL_HORIZONTAL); + gd.horizontalSpan= 2; + codeMiningGroup.setLayoutData(gd); + label= TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_show; - String description= TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_description; + 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)); @@ -1005,13 +1029,13 @@ public void widgetSelected(SelectionEvent e) { TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_ErrorWarnings)); codeMiningsDomain.addValue(new EnumValue(AnnotationCodeMiningPreferenceConstants.SHOW_ANNOTATION_CODE_MINING_LEVEL__ERROR_WARNING_INFO, TextEditorMessages.TextEditorDefaultsPreferencePage_codeMinings_ErrorWarningsInfo)); - final Control[] showCodeMiningsControls= addCombo(appearanceComposite, showCodeMinings, codeMiningsDomain, 0); + final Control[] showCodeMiningsControls= addCombo(codeMiningGroup, 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); + Control[] maxCodeMiningsControls= addTextField(codeMiningGroup, maxCodeMinings, maxCodeMiningsDomain, 15, 20); final SelectionListener codeMiningsListener= new SelectionAdapter() { @Override @@ -1027,16 +1051,17 @@ public void widgetSelected(SelectionEvent e) { ((Combo) showCodeMiningsControls[1]).addSelectionListener(codeMiningsListener); fMasterSlaveListeners.add(codeMiningsListener); - label= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingEnabled; - Preference stickyScrollingEnabled= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_ENABLED, label, null); - Button stickyScrollingEnabledButton= addCheckBox(appearanceComposite, stickyScrollingEnabled, new BooleanDomain(), 0); - - label= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount; - description= TextEditorMessages.TextEditorDefaultsPreferencePage_stickyScrollingMaximumCount; - Preference stickyScrollingMaximumCount= new Preference(AbstractDecoratedTextEditorPreferenceConstants.EDITOR_STICKY_SCROLLING_MAXIMUM_COUNT, label, description); - final IntegerDomain stickyScrollingMaximumCountDomain= new IntegerDomain(1, 10); - final Control[] stickyScrollingMaximumCountControls= addTextField(appearanceComposite, stickyScrollingMaximumCount, stickyScrollingMaximumCountDomain, 15, 20); - createDependency(stickyScrollingEnabledButton, stickyScrollingEnabled, stickyScrollingMaximumCountControls); + label= TextEditorMessages.TextEditorDefaultsPreferencePage_blockEndCodeMining; + description= TextEditorMessages.TextEditorDefaultsPreferencePage_blockEndCodeMining_description; + Preference blockEndCodeMining= new Preference(BlockEndCodeMiningPreferenceConstants.SHOW_BLOCK_END_CODE_MINING, label, description); + Button blockEndCodeMiningButton= addCheckBox(codeMiningGroup, blockEndCodeMining, new BooleanDomain(), 0); + + label= TextEditorMessages.TextEditorDefaultsPreferencePage_blockEndCodeMining_minLines; + description= TextEditorMessages.TextEditorDefaultsPreferencePage_blockEndCodeMining_minLines_description; + Preference blockEndCodeMiningMinLines= new Preference(BlockEndCodeMiningPreferenceConstants.BLOCK_END_CODE_MINING_MIN_LINES, label, description); + IntegerDomain blockEndCodeMiningMinLinesDomain= new IntegerDomain(2, 99999); + Control[] blockEndCodeMiningMinLinesControls= addTextField(codeMiningGroup, blockEndCodeMiningMinLines, blockEndCodeMiningMinLinesDomain, 15, 20); + createDependency(blockEndCodeMiningButton, blockEndCodeMining, blockEndCodeMiningMinLinesControls); addFiller(appearanceComposite, 2); diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java index fdcd1091798..6d2b0d0cfb0 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.java @@ -148,6 +148,11 @@ private TextEditorMessages() { public static String TextEditorDefaultsPreferencePage_codeMinings_max_description; public static String TextEditorDefaultsPreferencePage_codeMinings_none; public static String TextEditorDefaultsPreferencePage_codeMinings_show; + public static String TextEditorDefaultsPreferencePage_codeMinings_group; + public static String TextEditorDefaultsPreferencePage_blockEndCodeMining; + public static String TextEditorDefaultsPreferencePage_blockEndCodeMining_description; + public static String TextEditorDefaultsPreferencePage_blockEndCodeMining_minLines; + public static String TextEditorDefaultsPreferencePage_blockEndCodeMining_minLines_description; public static String TextEditorDefaultsPreferencePage_configureWhitespaceCharacterPainterProperties; public static String TextEditorDefaultsPreferencePage_deleteSpacesAsTabs; public static String TextEditorDefaultsPreferencePage_enclosed; diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties index 86575f8f274..68e6a429dc6 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/editors/text/TextEditorMessages.properties @@ -50,6 +50,11 @@ 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 problem annotations: +TextEditorDefaultsPreferencePage_codeMinings_group=Code Minings +TextEditorDefaultsPreferencePage_blockEndCodeMining=Show &block end code minings +TextEditorDefaultsPreferencePage_blockEndCodeMining_description=Shows the opening line of a code block at its closing brace +TextEditorDefaultsPreferencePage_blockEndCodeMining_minLines=Minimum block size (lines): +TextEditorDefaultsPreferencePage_blockEndCodeMining_minLines_description=A block end code mining is only shown when the block spans at least this many lines 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/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java index 2f50bf9e00e..9fa198ae07a 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/texteditor/AbstractDecoratedTextEditorPreferenceConstants.java @@ -28,6 +28,7 @@ import org.eclipse.ui.internal.editors.text.EditorsPlugin; import org.eclipse.ui.internal.editors.text.EditorsPluginPreferenceInitializer; +import org.eclipse.ui.internal.texteditor.BlockEndCodeMiningPreferenceConstants; import org.eclipse.ui.texteditor.spelling.SpellingService; @@ -883,6 +884,9 @@ public static void initializeDefaultValues(IPreferenceStore store) { store.setDefault(EDITOR_STICKY_SCROLLING_ENABLED, false); store.setDefault(EDITOR_STICKY_SCROLLING_MAXIMUM_COUNT, 4); + store.setDefault(BlockEndCodeMiningPreferenceConstants.SHOW_BLOCK_END_CODE_MINING, false); + store.setDefault(BlockEndCodeMiningPreferenceConstants.BLOCK_END_CODE_MINING_MIN_LINES, BlockEndCodeMiningPreferenceConstants.DEFAULT_MIN_LINES); + MarkerAnnotationPreferences.initializeDefaultValues(store); EditorsPluginPreferenceInitializer.setThemeBasedPreferences(store, false); diff --git a/bundles/org.eclipse.ui.workbench.texteditor/plugin.properties b/bundles/org.eclipse.ui.workbench.texteditor/plugin.properties index 0319142a6c4..0f48e163579 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/plugin.properties +++ b/bundles/org.eclipse.ui.workbench.texteditor/plugin.properties @@ -232,3 +232,4 @@ blockSelectionModeFont.description= The block selection mode font is used by tex MinimapView.name=Minimap CodeMining.show.ZWSP=Show ZWSP (Zero-Width Space) +CodeMining.blockEnd=Show block end code minings diff --git a/bundles/org.eclipse.ui.workbench.texteditor/plugin.xml b/bundles/org.eclipse.ui.workbench.texteditor/plugin.xml index 47d2f1a1a34..f910e647a7e 100644 --- a/bundles/org.eclipse.ui.workbench.texteditor/plugin.xml +++ b/bundles/org.eclipse.ui.workbench.texteditor/plugin.xml @@ -1554,6 +1554,11 @@ id="org.eclipse.test.codemining.zswpProvider" label="%CodeMining.show.ZWSP"> + + + * The keys are shared between the provider (which reads them) and the + * {@code org.eclipse.ui.editors} preference page and default initializer (which + * expose and write them). They therefore live in this package which is a friend + * of {@code org.eclipse.ui.editors}. + *

+ */ +public final class BlockEndCodeMiningPreferenceConstants { + + /** + * Boolean preference that controls whether a code mining echoing a block's + * opening line is shown at its closing brace. The default value is + * false. + */ + public static final String SHOW_BLOCK_END_CODE_MINING= "showBlockEndCodeMining"; //$NON-NLS-1$ + + /** + * Integer preference for the minimum number of lines a block must span before a + * block-end code mining is shown for it. + */ + public static final String BLOCK_END_CODE_MINING_MIN_LINES= "blockEndCodeMiningMinLines"; //$NON-NLS-1$ + + /** + * Default value for {@link #BLOCK_END_CODE_MINING_MIN_LINES}. + */ + public static final int DEFAULT_MIN_LINES= 20; + + private BlockEndCodeMiningPreferenceConstants() { + // Prevent instantiation + } +} diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMining.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMining.java new file mode 100644 index 00000000000..68ffa708619 --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMining.java @@ -0,0 +1,42 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Platform contributors. + * + * 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 + *******************************************************************************/ +package org.eclipse.ui.internal.texteditor.codemining; + +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.LineEndCodeMining; + +/** + * A code mining drawn at the end of a block's closing brace line that echoes the + * text of the block's opening line. + * + * @see BlockEndCodeMiningProvider + */ +class BlockEndCodeMining extends LineEndCodeMining { + + private final String label; + + BlockEndCodeMining(IDocument document, int line, String label, ICodeMiningProvider provider) throws BadLocationException { + super(document, line, provider); + this.label= label; + } + + @Override + public boolean isResolved() { + return true; + } + + @Override + public String getLabel() { + return label; + } +} diff --git a/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMiningProvider.java b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMiningProvider.java new file mode 100644 index 00000000000..450b22630de --- /dev/null +++ b/bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMiningProvider.java @@ -0,0 +1,237 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Platform contributors. + * + * 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 + *******************************************************************************/ +package org.eclipse.ui.internal.texteditor.codemining; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.CompletableFuture; + +import org.eclipse.core.runtime.IProgressMonitor; + +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.IDocumentExtension3; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.ITextViewer; +import org.eclipse.jface.text.ITypedRegion; +import org.eclipse.jface.text.TextUtilities; +import org.eclipse.jface.text.TypedRegion; +import org.eclipse.jface.text.codemining.AbstractCodeMiningProvider; +import org.eclipse.jface.text.codemining.ICodeMining; +import org.eclipse.jface.text.source.ISourceViewerExtension5; + +import org.eclipse.ui.internal.texteditor.BlockEndCodeMiningPreferenceConstants; + +/** + * A language-agnostic code mining provider that echoes the opening line of a + * curly-brace block at its closing brace, helping to identify which block a + * closing brace belongs to. + *

+ * To avoid cluttering the editor, a block is only annotated when it spans at + * least {@link BlockEndCodeMiningPreferenceConstants#BLOCK_END_CODE_MINING_MIN_LINES} + * lines. The feature is disabled by default and controlled through the + * preferences. + *

+ *

+ * Braces are matched purely structurally. When the document provides a + * partitioner for the {@link IDocumentExtension3#DEFAULT_PARTITIONING default + * partitioning}, braces inside non-default partitions (typically comments and + * strings) are ignored; otherwise the whole document is treated as code. + *

+ */ +public class BlockEndCodeMiningProvider extends AbstractCodeMiningProvider implements IPropertyChangeListener { + + /** + * Immutable description of a block whose closing brace should be annotated. + * + * @param endLine the zero-based document line of the closing brace + * @param label the label rendered at the closing brace, echoing the block's opening line + */ + public record BlockEnd(int endLine, String label) { + } + + private static final int MAX_LABEL_LENGTH= 100; + + /** Marks the echoed text as an annotation, following the usual hand-written closing brace comment. */ + private static final String LABEL_PREFIX= "// "; //$NON-NLS-1$ + + private IPreferenceStore store; + private boolean enabled; + private int minLines; + + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, IProgressMonitor monitor) { + if (store == null) { + loadStore(); + } + IDocument document= viewer.getDocument(); + if (!enabled || document == null) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + List minings= new ArrayList<>(); + for (BlockEnd blockEnd : computeBlockEnds(document, minLines, monitor)) { + if (monitor != null && monitor.isCanceled()) { + break; + } + try { + minings.add(new BlockEndCodeMining(document, blockEnd.endLine(), blockEnd.label(), this)); + } catch (BadLocationException e) { + // Skip minings that can no longer be positioned. + } + } + return CompletableFuture.completedFuture(minings); + } + + /** + * Computes the blocks that should be annotated in the given document. + * + * @param document the document to scan + * @param minLines the minimum number of lines a block must span to be annotated + * @param monitor the progress monitor to check for cancellation, may be null + * @return the closing braces to annotate together with the echoed opening line + */ + public static List computeBlockEnds(IDocument document, int minLines, IProgressMonitor monitor) { + List result= new ArrayList<>(); + if (document == null || document.getLength() == 0) { + return result; + } + Deque openBraces= new ArrayDeque<>(); + try { + for (ITypedRegion region : computeCodeRegions(document)) { + if (monitor != null && monitor.isCanceled()) { + return result; + } + if (!IDocument.DEFAULT_CONTENT_TYPE.equals(region.getType())) { + continue; + } + int regionOffset= region.getOffset(); + String code= document.get(regionOffset, region.getLength()); + for (int i= 0; i < code.length(); i++) { + char c= code.charAt(i); + if (c == '{') { + openBraces.push(regionOffset + i); + } else if (c == '}' && !openBraces.isEmpty()) { + collectIfSignificant(document, openBraces.pop(), regionOffset + i, minLines, result); + } + } + } + } catch (BadLocationException e) { + // Return what has been collected so far. + } + return result; + } + + private static ITypedRegion[] computeCodeRegions(IDocument document) throws BadLocationException { + ITypedRegion[] partitions= TextUtilities.computePartitioning(document, IDocumentExtension3.DEFAULT_PARTITIONING, 0, + document.getLength(), false); + if (partitions.length > 0) { + return partitions; + } + // No partitioner installed for the default partitioning; treat the whole + // document as code. + return new ITypedRegion[] { new TypedRegion(0, document.getLength(), IDocument.DEFAULT_CONTENT_TYPE) }; + } + + private static void collectIfSignificant(IDocument document, int openOffset, int closeOffset, int minLines, + List result) throws BadLocationException { + int openLine= document.getLineOfOffset(openOffset); + int closeLine= document.getLineOfOffset(closeOffset); + int lineCount= closeLine - openLine + 1; + // A single line block never needs a marker, both braces are visible at once. + if (lineCount < Math.max(2, minLines)) { + return; + } + String label= computeLabel(document, openOffset, openLine); + if (!label.isEmpty()) { + result.add(new BlockEnd(closeLine, LABEL_PREFIX + label)); + } + } + + private static String computeLabel(IDocument document, int openBraceOffset, int openLine) throws BadLocationException { + int lineOffset= document.getLineOffset(openLine); + String label= normalize(document.get(lineOffset, openBraceOffset - lineOffset)); + if (!label.isEmpty()) { + return label; + } + // The opening brace is the first token on its line (e.g. Allman style): use + // the closest preceding non-blank line instead. + for (int line= openLine - 1; line >= 0; line--) { + IRegion region= document.getLineInformation(line); + String text= normalize(document.get(region.getOffset(), region.getLength())); + if (!text.isEmpty()) { + return text; + } + } + return ""; //$NON-NLS-1$ + } + + private static String normalize(String text) { + String collapsed= text.replaceAll("\\s+", " ").trim(); //$NON-NLS-1$ //$NON-NLS-2$ + while (collapsed.endsWith("{")) { //$NON-NLS-1$ + collapsed= collapsed.substring(0, collapsed.length() - 1).trim(); + } + if (collapsed.length() > MAX_LABEL_LENGTH) { + collapsed= collapsed.substring(0, MAX_LABEL_LENGTH) + "\u2026"; //$NON-NLS-1$ + } + return collapsed; + } + + @Override + public void propertyChange(PropertyChangeEvent event) { + String property= event.getProperty(); + if (BlockEndCodeMiningPreferenceConstants.SHOW_BLOCK_END_CODE_MINING.equals(property) + || BlockEndCodeMiningPreferenceConstants.BLOCK_END_CODE_MINING_MIN_LINES.equals(property)) { + readPreferences(); + updateCodeMinings(); + } + } + + private void updateCodeMinings() { + ITextViewer viewer= getAdapter(ITextViewer.class); + if (viewer instanceof ISourceViewerExtension5 codeMiningExtension) { + codeMiningExtension.updateCodeMinings(); + } + } + + @Override + public void dispose() { + if (store != null) { + store.removePropertyChangeListener(this); + store= null; + } + super.dispose(); + } + + private void loadStore() { + store= getAdapter(IPreferenceStore.class); + readPreferences(); + if (store != null) { + store.addPropertyChangeListener(this); + } + } + + private void readPreferences() { + if (store == null) { + enabled= false; + minLines= BlockEndCodeMiningPreferenceConstants.DEFAULT_MIN_LINES; + return; + } + enabled= store.getBoolean(BlockEndCodeMiningPreferenceConstants.SHOW_BLOCK_END_CODE_MINING); + minLines= store.getInt(BlockEndCodeMiningPreferenceConstants.BLOCK_END_CODE_MINING_MIN_LINES); + } +} diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/BlockEndCodeMiningRenderingTest.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/BlockEndCodeMiningRenderingTest.java new file mode 100644 index 00000000000..9ef56fdb045 --- /dev/null +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/BlockEndCodeMiningRenderingTest.java @@ -0,0 +1,124 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Platform contributors. + * + * 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 + *******************************************************************************/ +package org.eclipse.ui.editors.tests; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.widgets.Control; + +import org.eclipse.core.filesystem.EFS; + +import org.eclipse.jface.preference.IPreferenceStore; + +import org.eclipse.jface.text.BadLocationException; +import org.eclipse.jface.text.IDocument; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.internal.editors.text.EditorsPlugin; +import org.eclipse.ui.tests.harness.util.DisplayHelper; + +import org.eclipse.ui.texteditor.AbstractTextEditor; + +/** + * Verifies that the block end code mining is rendered in a running text editor. + */ +public class BlockEndCodeMiningRenderingTest { + + private static final String SHOW_BLOCK_END_CODE_MINING= "showBlockEndCodeMining"; //$NON-NLS-1$ + + private static final String BLOCK_END_CODE_MINING_MIN_LINES= "blockEndCodeMiningMinLines"; //$NON-NLS-1$ + + /** Zero-based line of the closing brace in the test document. */ + private static final int CLOSING_BRACE_LINE= 5; + + private static final String SOURCE= "void method() {\n\ta();\n\tb();\n\tc();\n\td();\n}\n"; //$NON-NLS-1$ + + private IPreferenceStore store; + + private File file; + + private AbstractTextEditor editor; + + private StyledText widget; + + private IDocument document; + + @BeforeEach + void setUp() throws Exception { + store= EditorsPlugin.getDefault().getPreferenceStore(); + store.setValue(SHOW_BLOCK_END_CODE_MINING, true); + store.setValue(BLOCK_END_CODE_MINING_MIN_LINES, 5); + + file= File.createTempFile(BlockEndCodeMiningRenderingTest.class.getName(), ".txt"); //$NON-NLS-1$ + Files.write(file.toPath(), SOURCE.getBytes(StandardCharsets.UTF_8)); + editor= (AbstractTextEditor) IDE.openEditorOnFileStore( + PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage(), EFS.getStore(file.toURI())); + document= editor.getDocumentProvider().getDocument(editor.getEditorInput()); + widget= (StyledText) editor.getAdapter(Control.class); + } + + @AfterEach + void tearDown() { + store.setToDefault(SHOW_BLOCK_END_CODE_MINING); + store.setToDefault(BLOCK_END_CODE_MINING_MIN_LINES); + editor.close(false); + file.delete(); + TestUtil.cleanUp(); + } + + @Test + void miningIsRenderedAtTheClosingBrace() { + assertTrue(DisplayHelper.waitForCondition(widget.getDisplay(), 10_000, () -> reservesSpaceForMining(CLOSING_BRACE_LINE)), + "no code mining rendered at the closing brace"); //$NON-NLS-1$ + } + + @Test + void disablingThePreferenceRemovesTheMining() { + assertTrue(DisplayHelper.waitForCondition(widget.getDisplay(), 10_000, () -> reservesSpaceForMining(CLOSING_BRACE_LINE)), + "no code mining rendered at the closing brace"); //$NON-NLS-1$ + + store.setValue(SHOW_BLOCK_END_CODE_MINING, false); + + assertTrue(DisplayHelper.waitForCondition(widget.getDisplay(), 10_000, () -> !reservesSpaceForMining(CLOSING_BRACE_LINE)), + "code mining still rendered after disabling the preference"); //$NON-NLS-1$ + } + + /** + * Returns whether the given line carries a style range that reserves horizontal + * space, which is how a line content code mining is drawn. + */ + private boolean reservesSpaceForMining(int line) { + try { + int offset= document.getLineOffset(line); + int end= Math.min(offset + document.getLineLength(line), widget.getCharCount() - 1); + for (int i= offset; i <= end; i++) { + StyleRange range= widget.getStyleRangeAtOffset(i); + if (range != null && range.metrics != null && range.metrics.width > 0) { + return true; + } + } + return false; + } catch (BadLocationException e) { + return false; + } + } +} diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java index e8353b3e803..809a63f43d0 100644 --- a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/EditorsTestSuite.java @@ -49,6 +49,8 @@ LargeFileTest.class, CaseActionTest.class, TextMultiCaretNavigationTest.class, TextMultiCaretSelectionCommandsTest.class, + TextEditorDefaultsPreferencePageTest.class, + BlockEndCodeMiningRenderingTest.class, StickyScrollingControlTest.class, StickyScrollingHandlerTest.class, diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextEditorDefaultsPreferencePageTest.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextEditorDefaultsPreferencePageTest.java new file mode 100644 index 00000000000..d09ce683a0f --- /dev/null +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextEditorDefaultsPreferencePageTest.java @@ -0,0 +1,125 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Platform contributors. + * + * 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 + *******************************************************************************/ +package org.eclipse.ui.editors.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.FillLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Event; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.swt.widgets.Text; + +import org.eclipse.jface.preference.IPreferenceStore; + +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.editors.text.EditorsUI; +import org.eclipse.ui.internal.editors.text.TextEditorDefaultsPreferencePage; + +/** + * Tests the code mining settings on the Text Editors preference page. + */ +public class TextEditorDefaultsPreferencePageTest { + + /** Persisted key of the block end code mining preference, must stay stable. */ + private static final String SHOW_BLOCK_END_CODE_MINING= "showBlockEndCodeMining"; //$NON-NLS-1$ + + private static final String BLOCK_END_CODE_MINING_MIN_LINES= "blockEndCodeMiningMinLines"; //$NON-NLS-1$ + + private Shell shell; + + private TextEditorDefaultsPreferencePage page; + + @BeforeEach + public void setUp() { + shell= new Shell(PlatformUI.getWorkbench().getDisplay()); + shell.setLayout(new FillLayout()); + page= new TextEditorDefaultsPreferencePage(); + page.init(PlatformUI.getWorkbench()); + page.createControl(shell); + } + + @AfterEach + public void tearDown() { + page.dispose(); + shell.dispose(); + } + + @Test + public void blockEndCodeMiningDefaultsAreRegistered() { + IPreferenceStore store= EditorsUI.getPreferenceStore(); + + assertFalse(store.getDefaultBoolean(SHOW_BLOCK_END_CODE_MINING)); + assertEquals(20, store.getDefaultInt(BLOCK_END_CODE_MINING_MIN_LINES)); + } + + @Test + public void codeMiningSettingsShareOneGroup() { + List groups= collect(shell, Group.class); + + assertEquals(1, groups.size(), "expected the code mining settings to be the only group on the page"); //$NON-NLS-1$ + Group group= groups.get(0); + assertFalse(group.getText().isEmpty(), "the group needs a title"); //$NON-NLS-1$ + // The annotation level combo with its maximum count field, and the block end + // check box with its minimum block size field. + assertEquals(1, collect(group, Combo.class).size()); + assertEquals(1, collect(group, Button.class).size()); + assertEquals(2, collect(group, Text.class).size()); + } + + @Test + public void minimumBlockSizeFollowsTheBlockEndCheckBox() { + Group group= collect(shell, Group.class).get(0); + Button blockEndCheckBox= collect(group, Button.class).get(0); + Text minimumBlockSize= collect(group, Text.class).get(1); + + assertFalse(blockEndCheckBox.getSelection(), "the feature is opt-in"); //$NON-NLS-1$ + assertFalse(minimumBlockSize.getEnabled()); + + select(blockEndCheckBox, true); + assertTrue(minimumBlockSize.getEnabled()); + + select(blockEndCheckBox, false); + assertFalse(minimumBlockSize.getEnabled()); + } + + private static void select(Button button, boolean selected) { + button.setSelection(selected); + button.notifyListeners(SWT.Selection, new Event()); + } + + private static List collect(Composite parent, Class type) { + List found= new ArrayList<>(); + for (Control child : parent.getChildren()) { + if (type.isInstance(child)) { + found.add(type.cast(child)); + } + if (child instanceof Composite composite) { + found.addAll(collect(composite, type)); + } + } + return found; + } +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/BlockEndCodeMiningTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/BlockEndCodeMiningTest.java new file mode 100644 index 00000000000..63cc9f0d157 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/BlockEndCodeMiningTest.java @@ -0,0 +1,307 @@ +/******************************************************************************* + * Copyright (c) 2026 Eclipse Platform contributors. + * + * 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 + *******************************************************************************/ +package org.eclipse.ui.workbench.texteditor.tests; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import org.eclipse.core.runtime.NullProgressMonitor; + +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentPartitioner; +import org.eclipse.jface.text.rules.EndOfLineRule; +import org.eclipse.jface.text.rules.FastPartitioner; +import org.eclipse.jface.text.rules.IPredicateRule; +import org.eclipse.jface.text.rules.IToken; +import org.eclipse.jface.text.rules.MultiLineRule; +import org.eclipse.jface.text.rules.RuleBasedPartitionScanner; +import org.eclipse.jface.text.rules.SingleLineRule; +import org.eclipse.jface.text.rules.Token; + +import org.eclipse.ui.internal.texteditor.codemining.BlockEndCodeMiningProvider; +import org.eclipse.ui.internal.texteditor.codemining.BlockEndCodeMiningProvider.BlockEnd; + +/** + * Tests the structural block matching of {@link BlockEndCodeMiningProvider}. + */ +public class BlockEndCodeMiningTest { + + private static final int MIN_LINES= 20; + + private static final String COMMENT= "__comment"; //$NON-NLS-1$ + + private static final String STRING= "__string"; //$NON-NLS-1$ + + private static IDocument document(String text) { + return new Document(text); + } + + /** + * Returns a document with a partitioner that puts Java-like comments and string + * literals into their own partitions. + */ + private static IDocument partitionedDocument(String text) { + Document document= new Document(text); + IToken comment= new Token(COMMENT); + IToken string= new Token(STRING); + RuleBasedPartitionScanner scanner= new RuleBasedPartitionScanner(); + scanner.setPredicateRules(new IPredicateRule[] { + new EndOfLineRule("//", comment), //$NON-NLS-1$ + new MultiLineRule("/*", "*/", comment), //$NON-NLS-1$ //$NON-NLS-2$ + new SingleLineRule("\"", "\"", string, '\\'), //$NON-NLS-1$ //$NON-NLS-2$ + }); + IDocumentPartitioner partitioner= new FastPartitioner(scanner, new String[] { COMMENT, STRING }); + partitioner.connect(document); + document.setDocumentPartitioner(partitioner); + return document; + } + + private static List blockEnds(IDocument document) { + return blockEnds(document, MIN_LINES); + } + + private static List blockEnds(IDocument document, int minLines) { + return BlockEndCodeMiningProvider.computeBlockEnds(document, minLines, null); + } + + private static String body(int lines) { + return body(lines, "\n"); //$NON-NLS-1$ + } + + private static String body(int lines, String delimiter) { + StringBuilder builder= new StringBuilder(); + for (int i= 0; i < lines; i++) { + builder.append("\tstatement").append(i).append("();").append(delimiter); //$NON-NLS-1$ //$NON-NLS-2$ + } + return builder.toString(); + } + + @Test + public void longBlockIsAnnotated() { + IDocument document= document("void method() {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(26, ends.get(0).endLine()); + } + + @Test + public void shortBlockIsNotAnnotated() { + IDocument document= document("void method() {\n" + body(3) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertTrue(blockEnds(document).isEmpty()); + } + + @Test + public void blockOfExactlyMinLinesIsAnnotated() { + // The opening and the closing brace line count towards the block size. + IDocument document= document("void method() {\n" + body(MIN_LINES - 2) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals(MIN_LINES - 1, ends.get(0).endLine()); + } + + @Test + public void blockOneLineShorterThanMinLinesIsNotAnnotated() { + IDocument document= document("void method() {\n" + body(MIN_LINES - 3) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertTrue(blockEnds(document).isEmpty()); + } + + @Test + public void nestingAloneDoesNotQualifyAShortBlock() { + IDocument document= document("if (a) {\n\tif (b) {\n\t\tx();\n\t}\n}\n"); //$NON-NLS-1$ + + assertTrue(blockEnds(document).isEmpty()); + } + + @Test + public void singleLineBlockIsNotAnnotatedForAnyThreshold() { + IDocument document= document("if (a) { x(); }\n"); //$NON-NLS-1$ + + assertTrue(blockEnds(document, 1).isEmpty()); + assertTrue(blockEnds(document, 2).isEmpty()); + } + + @Test + public void nestedBlocksAreReportedInnermostFirst() { + IDocument document= document("outer() {\n\tinner() {\n" + body(25) + "\t}\n}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(2, ends.size()); + assertEquals("// inner()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(27, ends.get(0).endLine()); + assertEquals("// outer()", ends.get(1).label()); //$NON-NLS-1$ + assertEquals(28, ends.get(1).endLine()); + } + + @Test + public void siblingBlocksAreBothAnnotated() { + IDocument document= document("a() {\n" + body(25) + "}\nb() {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + List ends= blockEnds(document); + + assertEquals(2, ends.size()); + assertEquals("// a()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals("// b()", ends.get(1).label()); //$NON-NLS-1$ + } + + @Test + public void allmanStyleLabelUsesPrecedingLine() { + IDocument document= document("void method()\n{\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + } + + @Test + public void allmanStyleLabelSkipsBlankLines() { + IDocument document= document("void method()\n\n \n{\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + } + + @Test + public void blockWithoutAnyPrecedingTextIsNotAnnotated() { + IDocument document= document("{\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertTrue(blockEnds(document).isEmpty()); + } + + @Test + public void labelCollapsesWhitespaceAndStripsBraces() { + IDocument document= document("\tvoid\tmethod(int a) {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method(int a)", ends.get(0).label()); //$NON-NLS-1$ + } + + @Test + public void longLabelIsTruncated() { + String name= "m".repeat(200); //$NON-NLS-1$ + IDocument document= document("void " + name + "() {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + String label= ends.get(0).label(); + assertEquals("// ".length() + 100 + 1, label.length()); //$NON-NLS-1$ + assertTrue(label.endsWith("\u2026"), label); //$NON-NLS-1$ + } + + @Test + public void unbalancedClosingBraceIsIgnored() { + IDocument document= document("}\nvoid method() {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + } + + @Test + public void unbalancedOpeningBraceIsIgnored() { + IDocument document= document("void broken() {\n" + body(25)); //$NON-NLS-1$ + + assertTrue(blockEnds(document).isEmpty()); + } + + @Test + public void carriageReturnLineDelimitersAreSupported() { + IDocument document= document("void method() {\r\n" + body(25, "\r\n") + "}\r\n"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(26, ends.get(0).endLine()); + } + + @Test + public void emptyDocumentYieldsNoMinings() { + assertTrue(blockEnds(document("")).isEmpty()); //$NON-NLS-1$ + } + + @Test + public void nullDocumentYieldsNoMinings() { + assertTrue(blockEnds(null, MIN_LINES).isEmpty()); + } + + @Test + public void canceledMonitorStopsTheScan() { + IDocument document= document("void method() {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + NullProgressMonitor monitor= new NullProgressMonitor(); + monitor.setCanceled(true); + + assertTrue(BlockEndCodeMiningProvider.computeBlockEnds(document, MIN_LINES, monitor).isEmpty()); + } + + @Test + public void bracesInLineCommentsAreIgnored() { + IDocument document= partitionedDocument("void method() {\n\t// } not a block end {\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(27, ends.get(0).endLine()); + } + + @Test + public void bracesInBlockCommentsAreIgnored() { + IDocument document= partitionedDocument("void method() {\n\t/* }\n\t { */\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(28, ends.get(0).endLine()); + } + + @Test + public void bracesInStringLiteralsAreIgnored() { + IDocument document= partitionedDocument("void method() {\n\tString s= \"{}\";\n" + body(25) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(27, ends.get(0).endLine()); + } + + @Test + public void partitionedDocumentStillMatchesCodeBraces() { + IDocument document= partitionedDocument("void method() { // opening\n" + body(25) + "} // closing\n"); //$NON-NLS-1$ //$NON-NLS-2$ + + List ends= blockEnds(document); + + assertEquals(1, ends.size()); + assertEquals("// void method()", ends.get(0).label()); //$NON-NLS-1$ + assertEquals(26, ends.get(0).endLine()); + } +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java index 492442c11be..8b90b78fb70 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/WorkbenchTextEditorTestSuite.java @@ -50,6 +50,7 @@ FindReplaceOverlayTest.class, FindReplaceDialogTest.class, HistoryStoreTest.class, + BlockEndCodeMiningTest.class, }) public class WorkbenchTextEditorTestSuite { // see @SelectClasses