From 8a4d2f4407cc7b13c85db1452f5ebe2a18cb26cc Mon Sep 17 00:00:00 2001
From: Lars Vogel
Date: Wed, 8 Jul 2026 11:24:46 +0200
Subject: [PATCH] Add opt-in block-end code mining
Add a language-agnostic code mining that echoes a block's opening line at
its closing brace, making it easier to see which block a closing brace
belongs to. The echoed text is prefixed with "// " so it reads like the
closing brace comment people write by hand.
Braces are matched purely structurally, so braces inside comments and
strings are ignored when the document has a partitioner for the default
partitioning; otherwise the whole document is treated as code. There is
no dependency on JDT or any language model.
To avoid clutter the mining is only shown for blocks spanning at least a
configurable number of lines (default 20). The feature is registered by
default but disabled via preferences and can be enabled together with
the minimum block size on the Text Editors preference page. The code
mining settings there now live in their own group, which also takes the
existing problem annotation settings off the crowded flat option list.
---
.../TextEditorDefaultsPreferencePage.java | 51 ++-
.../editors/text/TextEditorMessages.java | 5 +
.../text/TextEditorMessages.properties | 5 +
...ecoratedTextEditorPreferenceConstants.java | 4 +
.../plugin.properties | 1 +
.../plugin.xml | 5 +
...BlockEndCodeMiningPreferenceConstants.java | 46 +++
.../codemining/BlockEndCodeMining.java | 42 +++
.../BlockEndCodeMiningProvider.java | 237 ++++++++++++++
.../BlockEndCodeMiningRenderingTest.java | 124 +++++++
.../ui/editors/tests/EditorsTestSuite.java | 2 +
.../TextEditorDefaultsPreferencePageTest.java | 125 +++++++
.../tests/BlockEndCodeMiningTest.java | 307 ++++++++++++++++++
.../tests/WorkbenchTextEditorTestSuite.java | 1 +
14 files changed, 942 insertions(+), 13 deletions(-)
create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/BlockEndCodeMiningPreferenceConstants.java
create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMining.java
create mode 100644 bundles/org.eclipse.ui.workbench.texteditor/src/org/eclipse/ui/internal/texteditor/codemining/BlockEndCodeMiningProvider.java
create mode 100644 tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/BlockEndCodeMiningRenderingTest.java
create mode 100644 tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextEditorDefaultsPreferencePageTest.java
create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/BlockEndCodeMiningTest.java
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