From 308cb73b3429d6e1de29b05896a15172429637f8 Mon Sep 17 00:00:00 2001 From: danthe1st Date: Wed, 25 Dec 2024 20:56:43 +0100 Subject: [PATCH 1/2] add custom folding regions --- .../folding/CustomFoldingRegionTest.java | 430 ++++++++++++++++++ .../jdt/ui/tests/folding/FoldingTest.java | 11 + .../DefaultJavaFoldingPreferenceBlock.java | 82 +++- .../ui/text/folding/FoldingMessages.java | 5 + .../text/folding/FoldingMessages.properties | 7 +- .../eclipse/jdt/ui/PreferenceConstants.java | 33 ++ .../DefaultJavaFoldingStructureProvider.java | 142 +++++- 7 files changed, 684 insertions(+), 26 deletions(-) create mode 100644 org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/CustomFoldingRegionTest.java create mode 100644 org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/FoldingTest.java diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/CustomFoldingRegionTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/CustomFoldingRegionTest.java new file mode 100644 index 00000000000..468183d2c9e --- /dev/null +++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/CustomFoldingRegionTest.java @@ -0,0 +1,430 @@ +package org.eclipse.jdt.ui.tests.folding; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; + +import org.eclipse.jdt.testplugin.JavaProjectHelper; + +import org.eclipse.core.runtime.CoreException; + +import org.eclipse.jface.preference.IPreferenceStore; + +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Position; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.source.Annotation; +import org.eclipse.jface.text.source.projection.ProjectionAnnotation; +import org.eclipse.jface.text.source.projection.ProjectionAnnotationModel; + +import org.eclipse.ui.PartInitException; + +import org.eclipse.jdt.core.ICompilationUnit; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragment; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaModelException; + +import org.eclipse.jdt.ui.PreferenceConstants; +import org.eclipse.jdt.ui.tests.core.rules.ProjectTestSetup; + +import org.eclipse.jdt.internal.ui.JavaPlugin; +import org.eclipse.jdt.internal.ui.javaeditor.EditorUtility; +import org.eclipse.jdt.internal.ui.javaeditor.JavaEditor; + +public class CustomFoldingRegionTest { + @Rule + public ProjectTestSetup projectSetup= new ProjectTestSetup(); + + private IJavaProject fJProject1; + + private IPackageFragmentRoot fSourceFolder; + + private IPackageFragment fPackageFragment; + + @Before + public void setUp() throws CoreException { + fJProject1= projectSetup.getProject(); + fSourceFolder= JavaProjectHelper.addSourceContainer(fJProject1, "src"); + fPackageFragment= fSourceFolder.createPackageFragment("org.example.test", false, null); + } + + + @After + public void tearDown() throws CoreException { + JavaProjectHelper.delete(fJProject1); + IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore(); + store.setToDefault(PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_START); + store.setToDefault(PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_END); + } + + @Test + public void testNoCustomFoldingRegions() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + public class Test { + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(0, projectionRanges.size()); + } + + @Test + public void testCustomFoldingRegionInsideAndOutsideClass() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + // #region + // something else + // #endregion + public class Test { + // #region + // something else + // #endregion + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(2, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 1, 3); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 5, 7); + } + + @Test + public void testNestedCustomRegions() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + public class Test { + // #region outer + // #region inner + + // #endregion outer + // #endregion inner + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(2, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 7);//outer + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 4, 6);//inner + } + + @Test + public void testNoCustomFoldingRegionsInMethod() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + public class Test { + void a(){ + + } + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(1, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 2, 4); + } + + @Test + public void testCustomFoldingRegionsInMethod() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + public class Test { + void a(){ + // #region + + // #endregion + } + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(2, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 2, 6); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 5); + } + + @Test + public void testNoCustomFoldingRegionsSingleImport() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + import java.util.List; + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(0, projectionRanges.size()); + } + + @Test + public void testCustomFoldingRegionAroundSingleImport() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + // #region imports + import java.util.List; + // #endregion + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(1, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 2, 4); + } + + @Test + public void testCustomFoldingRegionAroundClasses() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + class A { + + } + + // #region + + class B { + + } + + class C { + + } + // #endregion + + class D { + + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(1, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 6, 15); + } + + @Test + public void testCustomFoldingRegionsMultipleLevels() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + // #region outside class + public class Test { + // #endregion should be ignored + // #region outside method + void a(){ + // #endregion should be ignored + // #region inside method + System.out.println("Hello World"); + // #endregion inside method + } + // #endregion outside method + } + // #endregion outside class + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(4, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 1, 13);//outside class + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 4, 11);//outside method + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 5, 10);//void a() + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 7, 9);//inside method + } + + @Test + public void testCustomFoldingRegionsNotEndingTooEarly() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + public class Test { + void a(){ + // #region inside method + } + // #endregion outside method + } + // #endregion outside class + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(1, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 5);//void a() + } + + @Test + public void testCustomFoldingRegionsUsingSpecialCommentTypes() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + public class Test { + void a(){ + /* #region multiline + */ + /** #region javadoc */ + /** #endregion javadoc */ + /* #endregion multiline + */ + } + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(3, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 10);//void a() + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 4, 8);// multiline + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 6, 7);// javadoc + } + + @Test + public void testCustomRegionsWithLocalClass() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + public class Test { + void a(){ + // #region + int i; + + // #endregion + class Inner{ + + } + } + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(3, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 11);//void a() + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 4, 7);//region + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 8, 10);//class Inner + } + + @Test + public void testNoCustomRegionAtDifferentLevelsWithOtherClass() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + public class Test{ + // #region outside + public class A { + public void helloWorld() { + } + // #endregion inside + } + + public class B { + } + + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(3, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 4, 8);//class A + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 5, 6);//void helloWorld() + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 10, 11);//class B + } + + @Test + public void testCustomRegionsAroundFieldAndMethod() throws JavaModelException, PartInitException { + String str= """ + package org.example.test; + + public class Test { + // #region + int a; + + void b(){ + + } + // #endregion + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(2, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 9);//region + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 6, 8);//void b() + } + + @Test + public void testDifferentConfiguration() throws PartInitException, JavaModelException { + IPreferenceStore store= JavaPlugin.getDefault().getPreferenceStore(); + try { + store.setValue(PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_START, "#regstart"); + store.setValue(PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_END, "#regend"); + + + String str= """ + package org.example.test; + public class Test { + // #region should be ignored + // #regstart this is the region + // #regend should end here + // #endregion should be ignored + } + """; + List projectionRanges= getProjectionRangesOfFile(str); + assertEquals(1, projectionRanges.size()); + assertContainsRegionUsingStartAndEndLine(projectionRanges, str, 3, 4); + } finally { + + } + } + + private void assertContainsRegionUsingStartAndEndLine(List projectionRanges, String input, int startLine, int endLine) { + assertTrue(startLine <= endLine, "start line must be smaller or equal to end line"); + int startLineBegin= findLineStartIndex(input, startLine); + + int endLineBegin= findLineStartIndex(input, endLine); + int endLineEnd= findNextLineStart(input, endLineBegin); + endLineEnd= getLengthIfNotFound(input, endLineEnd); + + for (IRegion region : projectionRanges) { + if (region.getOffset() == startLineBegin + 1 && region.getOffset() + region.getLength() == endLineEnd + 1) { + return; + } + } + + fail( + "missing region from line " + startLine + "(index " + (startLineBegin + 1) + ") " + + "to line " + endLine + "(index " + (endLineEnd + 1) + ")" + + ", actual regions: " + projectionRanges + ); + } + + + private int getLengthIfNotFound(String input, int startLineEnd) { + if (startLineEnd == -1) { + startLineEnd= input.length(); + } + return startLineEnd; + } + + + private int findLineStartIndex(String input, int lineNumber) { + int currentInputIndex= 0; + for (int i= 0; i < lineNumber; i++) { + currentInputIndex= findNextLineStart(input, currentInputIndex); + if (currentInputIndex == -1) { + fail("line number is greater than the total number of lines"); + } + } + return currentInputIndex; + } + + + private int findNextLineStart(String input, int currentInputIndex) { + return input.indexOf('\n', currentInputIndex + 1); + } + + private List getProjectionRangesOfFile(String str) throws JavaModelException, PartInitException { + + ICompilationUnit compilationUnit= fPackageFragment.createCompilationUnit("Test.java", str, false, null); + JavaEditor editor= (JavaEditor) EditorUtility.openInEditor(compilationUnit); + ProjectionAnnotationModel model= editor.getAdapter(ProjectionAnnotationModel.class); + List regions= new ArrayList<>(); + for (Iterator it= model.getAnnotationIterator(); it.hasNext();) { + Annotation annotation= it.next(); + if (annotation instanceof ProjectionAnnotation projectionAnnotation) { + Position position= model.getPosition(projectionAnnotation); + regions.add(new Region(position.getOffset(), position.getLength())); + } + } + return regions; + } + +} diff --git a/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/FoldingTest.java b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/FoldingTest.java new file mode 100644 index 00000000000..6bd715cad5f --- /dev/null +++ b/org.eclipse.jdt.ui.tests/ui/org/eclipse/jdt/ui/tests/folding/FoldingTest.java @@ -0,0 +1,11 @@ +package org.eclipse.jdt.ui.tests.folding; + +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; + +@Suite +@SelectClasses({ + CustomFoldingRegionTest.class +}) +public class FoldingTest { +} diff --git a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/folding/DefaultJavaFoldingPreferenceBlock.java b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/folding/DefaultJavaFoldingPreferenceBlock.java index b8f64cb15ab..f9f5e0e3ba2 100644 --- a/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/folding/DefaultJavaFoldingPreferenceBlock.java +++ b/org.eclipse.jdt.ui/ui/org/eclipse/jdt/internal/ui/text/folding/DefaultJavaFoldingPreferenceBlock.java @@ -15,10 +15,10 @@ import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.Map; import org.eclipse.swt.SWT; +import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; @@ -26,7 +26,9 @@ import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Group; import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Text; import org.eclipse.jface.preference.IPreferenceStore; @@ -59,6 +61,12 @@ public void widgetSelected(SelectionEvent e) { fOverlayStore.setValue(fCheckBoxes.get(button), button.getSelection()); } }; + private Map fStringInputs= new HashMap<>(); + private ModifyListener fModifyListener = e -> { + Text text = (Text)e.widget; + fOverlayStore.setValue(fStringInputs.get(text), text.getText()); + }; + public DefaultJavaFoldingPreferenceBlock() { @@ -75,6 +83,9 @@ private OverlayKey[] createKeys() { overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, PreferenceConstants.EDITOR_FOLDING_METHODS)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, PreferenceConstants.EDITOR_FOLDING_IMPORTS)); overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, PreferenceConstants.EDITOR_FOLDING_HEADERS)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.BOOLEAN, PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGIONS)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_START)); + overlayKeys.add(new OverlayPreferenceStore.OverlayKey(OverlayPreferenceStore.STRING, PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_END)); return overlayKeys.toArray(new OverlayKey[overlayKeys.size()]); } @@ -87,22 +98,37 @@ public Control createControl(Composite composite) { fOverlayStore.load(); fOverlayStore.start(); - Composite inner= new Composite(composite, SWT.NONE); GridLayout layout= new GridLayout(1, true); layout.verticalSpacing= 3; layout.marginWidth= 0; - inner.setLayout(layout); - - Label label= new Label(inner, SWT.LEFT); - label.setText(FoldingMessages.DefaultJavaFoldingPreferenceBlock_title); - - addCheckBox(inner, FoldingMessages.DefaultJavaFoldingPreferenceBlock_comments, PreferenceConstants.EDITOR_FOLDING_JAVADOC, 0); - addCheckBox(inner, FoldingMessages.DefaultJavaFoldingPreferenceBlock_headers, PreferenceConstants.EDITOR_FOLDING_HEADERS, 0); - addCheckBox(inner, FoldingMessages.DefaultJavaFoldingPreferenceBlock_innerTypes, PreferenceConstants.EDITOR_FOLDING_INNERTYPES, 0); - addCheckBox(inner, FoldingMessages.DefaultJavaFoldingPreferenceBlock_methods, PreferenceConstants.EDITOR_FOLDING_METHODS, 0); - addCheckBox(inner, FoldingMessages.DefaultJavaFoldingPreferenceBlock_imports, PreferenceConstants.EDITOR_FOLDING_IMPORTS, 0); - - return inner; + composite.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); + + Composite outer= new Composite(composite, SWT.NONE); + outer.setLayout(layout); + outer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + Group initialFoldingGroup= new Group(outer, SWT.NONE); + initialFoldingGroup.setLayout(layout); + initialFoldingGroup.setText(FoldingMessages.DefaultJavaFoldingPreferenceBlock_title); + initialFoldingGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + addCheckBox(initialFoldingGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_comments, PreferenceConstants.EDITOR_FOLDING_JAVADOC, 0); + addCheckBox(initialFoldingGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_headers, PreferenceConstants.EDITOR_FOLDING_HEADERS, 0); + addCheckBox(initialFoldingGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_innerTypes, PreferenceConstants.EDITOR_FOLDING_INNERTYPES, 0); + addCheckBox(initialFoldingGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_methods, PreferenceConstants.EDITOR_FOLDING_METHODS, 0); + addCheckBox(initialFoldingGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_imports, PreferenceConstants.EDITOR_FOLDING_IMPORTS, 0); + addCheckBox(initialFoldingGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_customRegions, PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGIONS, 0); + + Group customRegionGroup= new Group(outer, SWT.NONE); + customRegionGroup.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); + + GridLayout customRegionLayout= new GridLayout(2, false); + customRegionGroup.setLayout(customRegionLayout); + customRegionGroup.setText(FoldingMessages.DefaultJavaFoldingPreferenceBlock_custom_region_title); + addStringInput(customRegionGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_CustomRegionStart, PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_START); + addStringInput(customRegionGroup, FoldingMessages.DefaultJavaFoldingPreferenceBlock_CustomRegionEnd, PreferenceConstants.EDITOR_FOLDING_CUSTOM_REGION_END); + + return outer; } private Button addCheckBox(Composite parent, String label, String key, int indentation) { @@ -121,13 +147,29 @@ private Button addCheckBox(Composite parent, String label, String key, int inden return checkBox; } + private void addStringInput(Composite parent, String label, String key) { + Label labelElement = new Label(parent, SWT.LEFT); + labelElement.setText(label); + GridData labelGridData= new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING); + labelGridData.horizontalSpan= 1; + labelGridData.grabExcessVerticalSpace= false; + labelElement.setLayoutData(labelGridData); + + Text textInput = new Text(parent, SWT.SINGLE | SWT.BORDER); + textInput.setText(label); + textInput.addModifyListener(fModifyListener); + + GridData textGridData= new GridData(SWT.FILL, SWT.BEGINNING, true, false); + textGridData.horizontalSpan= 1; + textGridData.grabExcessVerticalSpace= true; + textInput.setLayoutData(textGridData); + + fStringInputs.put(textInput, key); + } + private void initializeFields() { - Iterator