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