diff --git a/bundles/org.eclipse.ui.editors/plugin.properties b/bundles/org.eclipse.ui.editors/plugin.properties index f43d57fe4fc..b41a2acee1f 100644 --- a/bundles/org.eclipse.ui.editors/plugin.properties +++ b/bundles/org.eclipse.ui.editors/plugin.properties @@ -125,6 +125,7 @@ preferenceKeywords.lineSpacing= line space height spacing preferenceKeywords.lineNumber= line numbers preferenceKeywords.printMargin= print margin preferenceKeywords.annotationCodeMining= annotation code mining marker error warning info +preferenceKeywords.blockEndCodeMining= block end code mining closing brace curly preferenceKeywords.annotations= annotations vertical ruler overview colors text editor preferenceKeywords.quickdiff= quick diff compare reference colors text editor changes preferenceKeywords.accessibility= accessibility caret cursor quick diff text editor ruler diff --git a/bundles/org.eclipse.ui.editors/plugin.xml b/bundles/org.eclipse.ui.editors/plugin.xml index 6d8ae756911..f1030e74d8b 100644 --- a/bundles/org.eclipse.ui.editors/plugin.xml +++ b/bundles/org.eclipse.ui.editors/plugin.xml @@ -288,6 +288,7 @@ + + 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 0d814966463..a2bac2d52b1 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"> + + + * Braces are matched structurally, without a parser: braces in comments and string literals are + * skipped where the document has a partitioner, and a document without one is treated as code in + * full, so a brace in a character literal can pair up wrongly there. + *

+ */ +public class BlockEndCodeMiningProvider extends AbstractCodeMiningProvider implements IPropertyChangeListener { + + /** + * A block whose closing brace should be annotated. + * + * @param endLine the zero-based line of the closing brace + * @param label the text to render there + */ + public record BlockEnd(int endLine, String label) { + } + + private static final int MAX_LABEL_LENGTH= 100; + + /** Reads like the closing brace comment people write by hand, the mining is drawn right after the brace. */ + private static final String LABEL_PREFIX= " // "; //$NON-NLS-1$ + + private static final Pattern WHITESPACE= Pattern.compile("\\s+"); //$NON-NLS-1$ + + /** How many characters to scan between two cancellation checks. */ + private static final int CANCELLATION_CHECK_INTERVAL= 8192; + + /** How long to wait after a document change before recomputing, in milliseconds. */ + private static final int UPDATE_DELAY= 500; + + private volatile IPreferenceStore store; + + private volatile boolean enabled; + + private volatile int minLines= BlockEndCodeMiningPreferenceConstants.DEFAULT_MIN_LINES; + + private final Runnable updater= this::updateCodeMinings; + + private final IDocumentListener documentListener= new IDocumentListener() { + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + // Nothing to do. + } + + @Override + public void documentChanged(DocumentEvent event) { + scheduleUpdate(); + } + }; + + private IDocument trackedDocument; + + private volatile boolean disposed; + + @Override + public CompletableFuture> provideCodeMinings(ITextViewer viewer, IProgressMonitor monitor) { + loadStore(); + IDocument document= viewer.getDocument(); + // Nothing in the platform recomputes minings on a document change, JDT brings its own + // reconciler for that, so follow the document here. + trackDocument(enabled ? document : null); + if (!enabled || document == null) { + return CompletableFuture.completedFuture(Collections.emptyList()); + } + int blockSize= minLines; + // Scanning a large document takes long enough to be felt, and this is called on the UI + // thread; the mining infrastructure waits for the future either way. + return CompletableFuture.supplyAsync(() -> collectMinings(document, blockSize, monitor)); + } + + private List collectMinings(IDocument document, int blockSize, IProgressMonitor monitor) { + List minings= new ArrayList<>(); + for (BlockEnd blockEnd : computeBlockEnds(document, blockSize, 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 minings; + } + + /** + * Computes the blocks that should be annotated in the given document. + * + * @param document the document to scan, may be null + * @param minLines the minimum number of lines a block must span + * @param monitor the monitor to check for cancellation, may be null + * @return the closing braces to annotate, innermost block first + */ + 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<>(); + int sinceLastCheck= 0; + 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++) { + if (++sinceLastCheck >= CANCELLATION_CHECK_INTERVAL) { + sinceLastCheck= 0; + if (monitor != null && monitor.isCanceled()) { + return result; + } + } + 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; + } + + /** + * Returns the partitions to scan for braces, so that comments and string literals can be left + * out. Only a partitioning that marks code as {@link IDocument#DEFAULT_CONTENT_TYPE} can say + * where the code is; TextMate for instance partitions every text file into content types of its + * own, and reading those as "no code here" would leave the document unannotated. A document + * with no such partitioning is treated as code in full. + */ + private static ITypedRegion[] computeCodeRegions(IDocument document) throws BadLocationException { + ITypedRegion[] wholeDocument= { new TypedRegion(0, document.getLength(), IDocument.DEFAULT_CONTENT_TYPE) }; + if (!(document instanceof IDocumentExtension3 extension)) { + return wholeDocument; + } + ITypedRegion[] partitions= computePartitioning(extension, document, IDocumentExtension3.DEFAULT_PARTITIONING); + if (partitions == null) { + // Editors like JDT register their partitioner under their own partitioning name only. + // Sorting keeps the choice reproducible, getPartitionings() is unordered. + String[] partitionings= extension.getPartitionings().clone(); + Arrays.sort(partitionings); + for (String partitioning : partitionings) { + if (IDocumentExtension3.DEFAULT_PARTITIONING.equals(partitioning)) { + continue; + } + partitions= computePartitioning(extension, document, partitioning); + if (partitions != null) { + break; + } + } + } + return partitions != null ? partitions : wholeDocument; + } + + /** + * Returns the partitioning of the document, or null when the given partitioning + * has no partitioner or does not mark any region as code. + */ + private static ITypedRegion[] computePartitioning(IDocumentExtension3 extension, IDocument document, + String partitioning) throws BadLocationException { + if (extension.getDocumentPartitioner(partitioning) == null) { + return null; + } + ITypedRegion[] partitions; + try { + partitions= extension.computePartitioning(partitioning, 0, document.getLength(), false); + } catch (BadPartitioningException e) { + return null; + } + for (ITypedRegion partition : partitions) { + if (IDocument.DEFAULT_CONTENT_TYPE.equals(partition.getType())) { + return partitions; + } + } + return null; + } + + 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= WHITESPACE.matcher(text).replaceAll(" ").trim(); //$NON-NLS-1$ + 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(store); + updateCodeMinings(); + } + } + + private void updateCodeMinings() { + ITextViewer viewer= getAdapter(ITextViewer.class); + if (viewer instanceof ISourceViewerExtension5 codeMiningExtension) { + codeMiningExtension.updateCodeMinings(); + } + } + + private synchronized void trackDocument(IDocument document) { + if (trackedDocument == document) { + return; + } + if (trackedDocument != null) { + trackedDocument.removeDocumentListener(documentListener); + } + trackedDocument= document; + if (document != null) { + document.addDocumentListener(documentListener); + } + } + + /** Recomputes once the typing has paused, timerExec collapses the pending updates into one. */ + private void scheduleUpdate() { + Display display= displayOfViewer(); + if (display != null) { + display.asyncExec(() -> { + if (!disposed) { + display.timerExec(UPDATE_DELAY, updater); + } + }); + } + } + + private Display displayOfViewer() { + ITextViewer viewer= getAdapter(ITextViewer.class); + StyledText widget= viewer != null ? viewer.getTextWidget() : null; + return widget != null && !widget.isDisposed() ? widget.getDisplay() : null; + } + + @Override + public synchronized void dispose() { + disposed= true; + Display display= displayOfViewer(); + if (display != null) { + display.timerExec(-1, updater); + } + trackDocument(null); + if (store != null) { + store.removePropertyChangeListener(this); + store= null; + } + super.dispose(); + } + + /** + * Reads the preferences and starts listening for their changes, once. Code minings are computed + * on a background thread, so this can be reached concurrently. + */ + private synchronized void loadStore() { + if (store != null) { + return; + } + IPreferenceStore preferenceStore= getAdapter(IPreferenceStore.class); + readPreferences(preferenceStore); + if (preferenceStore != null) { + preferenceStore.addPropertyChangeListener(this); + store= preferenceStore; + } + } + + private void readPreferences(IPreferenceStore preferenceStore) { + if (preferenceStore == null) { + enabled= false; + minLines= BlockEndCodeMiningPreferenceConstants.DEFAULT_MIN_LINES; + return; + } + enabled= preferenceStore.getBoolean(BlockEndCodeMiningPreferenceConstants.SHOW_BLOCK_END_CODE_MINING); + minLines= preferenceStore.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..e5401b4eeb4 --- /dev/null +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/BlockEndCodeMiningRenderingTest.java @@ -0,0 +1,141 @@ +/******************************************************************************* + * 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 { + + /** Persisted keys of the block end code mining preferences, they 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$ + + /** 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() { + waitForMining(); + } + + @Test + void miningEchoesTheOpeningLine() throws Exception { + int width= waitForMining(); + + // A longer opening line has to make the echoed text wider. + document.replace(0, "void method".length(), "void methodWithAConsiderablyLongerName"); //$NON-NLS-1$ //$NON-NLS-2$ + + assertTrue(DisplayHelper.waitForCondition(widget.getDisplay(), 10_000, () -> reservedWidth(CLOSING_BRACE_LINE) > width), + "the echoed text did not follow the opening line"); //$NON-NLS-1$ + } + + @Test + void disablingThePreferenceRemovesTheMining() { + waitForMining(); + + store.setValue(SHOW_BLOCK_END_CODE_MINING, false); + + assertTrue(DisplayHelper.waitForCondition(widget.getDisplay(), 10_000, () -> reservedWidth(CLOSING_BRACE_LINE) == 0), + "code mining still rendered after disabling the preference"); //$NON-NLS-1$ + } + + /** Waits for the mining to appear at the closing brace and returns the space it reserves. */ + private int waitForMining() { + assertTrue(DisplayHelper.waitForCondition(widget.getDisplay(), 10_000, () -> reservedWidth(CLOSING_BRACE_LINE) > 0), + "no code mining rendered at the closing brace"); //$NON-NLS-1$ + return reservedWidth(CLOSING_BRACE_LINE); + } + + /** + * Returns the horizontal space the given line reserves through glyph metrics, + * which is how a line content code mining is drawn. + */ + private int reservedWidth(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 range.metrics.width; + } + } + return 0; + } catch (BadLocationException e) { + return 0; + } + } +} 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..aaf02f93224 --- /dev/null +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/editors/tests/TextEditorDefaultsPreferencePageTest.java @@ -0,0 +1,133 @@ +/******************************************************************************* + * 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 keys of the block end code mining preferences, they 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() { + Group group= codeMiningGroup(); + + 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= codeMiningGroup(); + 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()); + } + + /** Returns the group holding the code mining settings, found by the check box it contains. */ + private Group codeMiningGroup() { + for (Group group : collect(shell, Group.class)) { + if (collect(group, Button.class).size() == 1 && collect(group, Combo.class).size() == 1) { + return group; + } + } + throw new AssertionError("no code mining group on the page"); //$NON-NLS-1$ + } + + 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..ee411307ca5 --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/BlockEndCodeMiningTest.java @@ -0,0 +1,408 @@ +/******************************************************************************* + * 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.IProgressMonitor; +import org.eclipse.core.runtime.NullProgressMonitor; + +import org.eclipse.jface.text.Document; +import org.eclipse.jface.text.DocumentEvent; +import org.eclipse.jface.text.IDocument; +import org.eclipse.jface.text.IDocumentPartitioner; +import org.eclipse.jface.text.ITypedRegion; +import org.eclipse.jface.text.TypedRegion; +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); + } + + private static final String TEXTMATE_LIKE= "tm4e.partitioning"; //$NON-NLS-1$ + + /** + * 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; + } + + /** + * Returns a document partitioned the way TextMate partitions every text file: under a + * partitioning of its own, and with no region of the default content type. + */ + private static IDocument foreignPartitionedDocument(String text) { + Document document= new Document(text); + IDocumentPartitioner partitioner= new SingleContentTypePartitioner("tm4e:base"); //$NON-NLS-1$ + document.setDocumentPartitioner(TEXTMATE_LIKE, partitioner); + partitioner.connect(document); + return document; + } + + /** Reports the whole document as one region of a content type of its own. */ + private static final class SingleContentTypePartitioner implements IDocumentPartitioner { + + private final String contentType; + + private IDocument document; + + SingleContentTypePartitioner(String contentType) { + this.contentType= contentType; + } + + @Override + public void connect(IDocument newDocument) { + document= newDocument; + } + + @Override + public void disconnect() { + document= null; + } + + @Override + public void documentAboutToBeChanged(DocumentEvent event) { + // Nothing to do. + } + + @Override + public boolean documentChanged(DocumentEvent event) { + return false; + } + + @Override + public String[] getLegalContentTypes() { + return new String[] { contentType }; + } + + @Override + public String getContentType(int offset) { + return contentType; + } + + @Override + public ITypedRegion[] computePartitioning(int offset, int length) { + return new ITypedRegion[] { new TypedRegion(0, document.getLength(), contentType) }; + } + + @Override + public ITypedRegion getPartition(int offset) { + return new TypedRegion(0, document.getLength(), contentType); + } + } + + 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 cancellationIsObservedWithinALargeScan() { + // One partition far larger than the cancellation check interval, so the scan + // has to notice the cancellation inside the character loop. + IDocument document= document("void method() {\n" + body(20000) + "}\n"); //$NON-NLS-1$ //$NON-NLS-2$ + IProgressMonitor monitor= new NullProgressMonitor() { + private int queries; + + @Override + public boolean isCanceled() { + return ++queries > 1; + } + }; + + 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() { + // The decoy brace is unbalanced, so a scan that does not skip strings would + // pair the closing brace with it and label the block from the string line. + 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 partitioningWithoutCodeRegionsIsIgnored() { + // TextMate installs such a partitioner on every text file. Reading it as "no code + // here" would leave every document unannotated. + IDocument document= foreignPartitionedDocument("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$ + } + + @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 3ae8d48c338..d15dac56e95 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 @@ -52,6 +52,7 @@ FindReplaceOverlayInEditorTest.class, FindReplaceDialogTest.class, HistoryStoreTest.class, + BlockEndCodeMiningTest.class, }) public class WorkbenchTextEditorTestSuite { // see @SelectClasses