From cc9941d9d75ad868dfd67562b882644332a9da5f Mon Sep 17 00:00:00 2001 From: Sebastian Thomschke Date: Wed, 15 Jan 2025 15:41:00 +0100 Subject: [PATCH] feat: support initial folding of license header --- .../lsp4e/test/folding/FoldingTest.java | 75 ++++++++++++++----- .../eclipse/lsp4e/test/utils/TestUtils.java | 18 +++++ .../LSPFoldingReconcilingStrategy.java | 35 ++++++++- .../lsp4e/ui/FoldingPreferencePage.java | 59 +++++++++++---- 4 files changed, 150 insertions(+), 37 deletions(-) diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/folding/FoldingTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/folding/FoldingTest.java index fe8496b54..72912c0a1 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/folding/FoldingTest.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/folding/FoldingTest.java @@ -12,64 +12,99 @@ import java.util.List; -import org.eclipse.core.resources.IFile; import org.eclipse.core.runtime.CoreException; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.lsp4e.LanguageServerPlugin; import org.eclipse.lsp4e.test.utils.AbstractTest; import org.eclipse.lsp4e.test.utils.TestUtils; import org.eclipse.lsp4e.tests.mock.MockLanguageServer; +import org.eclipse.lsp4e.ui.FoldingPreferencePage; import org.eclipse.lsp4j.FoldingRange; import org.eclipse.lsp4j.FoldingRangeKind; import org.eclipse.swt.custom.StyledText; import org.eclipse.swt.widgets.Control; import org.eclipse.ui.IEditorPart; -import org.eclipse.ui.PartInitException; import org.eclipse.ui.tests.harness.util.DisplayHelper; import org.junit.Test; public class FoldingTest extends AbstractTest { + private static final int MAX_WAIT_FOR_FOLDING = 3_000; + private static final String CONTENT = """ + /** + * SPDX-License-Identifier: EPL-2.0 + */ import import import + /** + * Some comment + */ visible """; @Test - public void testImportsFoldedByDefaultEnabled() throws CoreException { - collapseImports(true); + public void testLicenseHeaderAutoFolding() throws CoreException { + configureCollapse(FoldingPreferencePage.PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS, true); + configureCollapse(FoldingPreferencePage.PREF_AUTOFOLD_IMPORT_STATEMENTS, false); + IEditorPart editor = createEditor(); + + // wait for folding to happen + TestUtils.waitForAndAssertCondition(MAX_WAIT_FOR_FOLDING, () -> assertEquals(""" + /** + import + import + import + /** + * Some comment + */ + visible""", ((StyledText) editor.getAdapter(Control.class)).getText().trim())); + } + + @Test + public void testImportsAutoFolding() throws CoreException { + configureCollapse(FoldingPreferencePage.PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS, false); + configureCollapse(FoldingPreferencePage.PREF_AUTOFOLD_IMPORT_STATEMENTS, true); + IEditorPart editor = createEditor(); // wait for folding to happen - DisplayHelper.waitAndAssertCondition(editor.getSite().getShell().getDisplay(), - () -> assertEquals("import\nvisible", - ((StyledText) editor.getAdapter(Control.class)).getText().trim())); + TestUtils.waitForAndAssertCondition(MAX_WAIT_FOR_FOLDING, () -> assertEquals(""" + /** + * SPDX-License-Identifier: EPL-2.0 + */ + import + /** + * Some comment + */ + visible""", ((StyledText) editor.getAdapter(Control.class)).getText().trim())); } @Test - public void testImportsFoldedByDefaultDisabled() throws CoreException { - collapseImports(false); + public void testAutoFoldingDisabled() throws CoreException { + configureCollapse(FoldingPreferencePage.PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS, false); + configureCollapse(FoldingPreferencePage.PREF_AUTOFOLD_IMPORT_STATEMENTS, false); IEditorPart editor = createEditor(); // wait a few seconds before testing to ensure no folding happened - DisplayHelper.sleep(3000); + DisplayHelper.sleep(MAX_WAIT_FOR_FOLDING); assertEquals(CONTENT, ((StyledText) editor.getAdapter(Control.class)).getText()); } - private IEditorPart createEditor() throws CoreException, PartInitException { - IFile file = TestUtils.createUniqueTestFile(null, CONTENT); - final var foldingRange = new FoldingRange(0, 2); - foldingRange.setKind(FoldingRangeKind.Imports); - MockLanguageServer.INSTANCE.setFoldingRanges(List.of(foldingRange)); - IEditorPart editor = TestUtils.openEditor(file); - return editor; + private IEditorPart createEditor() throws CoreException { + final var foldingRangeLicense = new FoldingRange(0, 2); + foldingRangeLicense.setKind(FoldingRangeKind.Comment); + final var foldingRangeImport = new FoldingRange(3, 5); + foldingRangeImport.setKind(FoldingRangeKind.Imports); + MockLanguageServer.INSTANCE.setFoldingRanges(List.of(foldingRangeLicense, foldingRangeImport)); + + return TestUtils.openEditor(TestUtils.createUniqueTestFile(null, CONTENT)); } - private void collapseImports(boolean collapseImports) { + private void configureCollapse(String type, boolean collapse) { IPreferenceStore store = LanguageServerPlugin.getDefault().getPreferenceStore(); - store.setValue("foldingReconcilingStrategy.collapseImports", collapseImports); //$NON-NLS-1$ + store.setValue(type, collapse); + store.setValue(FoldingPreferencePage.PREF_AUTOFOLD_IMPORT_STATEMENTS, collapse); //$NON-NLS-1$ } - } diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/utils/TestUtils.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/utils/TestUtils.java index b5a8b9c5c..84f4841ec 100644 --- a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/utils/TestUtils.java +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/utils/TestUtils.java @@ -297,6 +297,24 @@ public static IEditorReference[] getEditors() { return null; } + public static void waitForAndAssertCondition(int timeout_ms, Runnable condition) { + waitForAndAssertCondition("Condition not met within expected time.", timeout_ms, condition); + } + + public static void waitForAndAssertCondition(int timeout_ms, Display display, Runnable condition) { + waitForAndAssertCondition("Condition not met within expected time.", timeout_ms, display, () -> { + condition.run(); + return true; + }); + } + + public static void waitForAndAssertCondition(String errorMessage, int timeout_ms, Runnable condition) { + waitForAndAssertCondition(errorMessage, timeout_ms, UI.getDisplay(), () -> { + condition.run(); + return true; + }); + } + public static void waitForAndAssertCondition(int timeout_ms, Condition condition) { waitForAndAssertCondition("Condition not met within expected time.", timeout_ms, condition); } diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/folding/LSPFoldingReconcilingStrategy.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/folding/LSPFoldingReconcilingStrategy.java index e85285c8c..e8c06afc5 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/folding/LSPFoldingReconcilingStrategy.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/folding/LSPFoldingReconcilingStrategy.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.regex.Pattern; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.jdt.annotation.Nullable; @@ -63,6 +64,9 @@ public class LSPFoldingReconcilingStrategy private static final Annotation[] NO_ANNOTATIONS = new Annotation[0]; + private static final Pattern LICENSE_KEYWORDS = Pattern + .compile("(?i)(copyright|licensed under|all rights reserved|SPDX-License-Identifier)"); //$NON-NLS-1$ + private @Nullable IDocument document; private @Nullable ProjectionAnnotationModel projectionAnnotationModel; private @Nullable ProjectionViewer viewer; @@ -72,6 +76,7 @@ public class LSPFoldingReconcilingStrategy private final IPreferenceStore prefStore = LanguageServerPlugin.getDefault().getPreferenceStore(); private boolean isFoldingEnabled = prefStore.getBoolean(FoldingPreferencePage.PREF_FOLDING_ENABLED); private boolean collapseComments = prefStore.getBoolean(FoldingPreferencePage.PREF_AUTOFOLD_COMMENTS); + private boolean collapseLicenseHeader = prefStore.getBoolean(FoldingPreferencePage.PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS); private boolean collapseFoldingRegions = prefStore.getBoolean(FoldingPreferencePage.PREF_AUTOFOLD_REGIONS); private boolean collapseImports = prefStore.getBoolean(FoldingPreferencePage.PREF_AUTOFOLD_IMPORT_STATEMENTS); private final IPropertyChangeListener foldingPrefsListener = (final PropertyChangeEvent event) -> { @@ -89,6 +94,9 @@ public class LSPFoldingReconcilingStrategy case FoldingPreferencePage.PREF_AUTOFOLD_COMMENTS: collapseComments = Boolean.parseBoolean(newValue.toString()); break; + case FoldingPreferencePage.PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS: + collapseLicenseHeader = Boolean.parseBoolean(newValue.toString()); + break; case FoldingPreferencePage.PREF_AUTOFOLD_REGIONS: collapseFoldingRegions = Boolean.parseBoolean(newValue.toString()); break; @@ -187,22 +195,29 @@ private void applyFolding(@Nullable List ranges) { markInvalidAnnotationsForDeletion(deletions, existing); if (ranges != null) { + boolean[] isFirstFoldingRange = { true }; ranges.stream() // .sorted(Comparator.comparing(FoldingRange::getEndLine)) // .forEach(foldingRange -> { try { final var collapsByDefault = foldingRange.getKind() != null && switch (foldingRange.getKind()) { - case FoldingRangeKind.Comment -> collapseComments; + case FoldingRangeKind.Comment -> { + if (isFirstFoldingRange[0] + && LICENSE_KEYWORDS.matcher(getTextOfFoldingRange(foldingRange)).find()) + yield collapseLicenseHeader || collapseComments; + yield collapseComments; + } case FoldingRangeKind.Imports -> collapseImports; case FoldingRangeKind.Region -> collapseFoldingRegions; default -> false; }; updateAnnotation(deletions, existing, additions, foldingRange.getStartLine(), foldingRange.getEndLine(), collapsByDefault); - } catch (BadLocationException e) { - // should never occur + } catch (BadLocationException ex) { + LanguageServerPlugin.logError(ex); } + isFirstFoldingRange[0] = false; }); } @@ -219,6 +234,20 @@ && switch (foldingRange.getKind()) { } } + private String getTextOfFoldingRange(final FoldingRange range) { + final var doc = this.document; + if (doc != null) { + try { + final int offsetStart = doc.getLineOffset(range.getStartLine()); + return doc.get(offsetStart, + doc.getLineOffset(range.getEndLine()) + doc.getLineLength(range.getEndLine()) - offsetStart); + } catch (BadLocationException ex) { + LanguageServerPlugin.logError(ex); + } + } + return ""; //$NON-NLS-1$ + } + @Override public void install(ITextViewer viewer) { if (this.viewer != null) { diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/FoldingPreferencePage.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/FoldingPreferencePage.java index af3002b36..73f935277 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/FoldingPreferencePage.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/ui/FoldingPreferencePage.java @@ -26,6 +26,7 @@ public class FoldingPreferencePage extends FieldEditorPreferencePage implements public static final String PREF_FOLDING_ENABLED = "foldingReconcilingStrategy.enabled"; //$NON-NLS-1$ public static final String PREF_AUTOFOLD_COMMENTS = "foldingReconcilingStrategy.collapseComments"; //$NON-NLS-1$ + public static final String PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS = "foldingReconcilingStrategy.collapseLicenseHeaders"; //$NON-NLS-1$ public static final String PREF_AUTOFOLD_REGIONS = "foldingReconcilingStrategy.collapseRegions"; //$NON-NLS-1$ public static final String PREF_AUTOFOLD_IMPORT_STATEMENTS = "foldingReconcilingStrategy.collapseImports"; //$NON-NLS-1$ @@ -35,6 +36,7 @@ public void initializeDefaultPreferences() { final var store = LanguageServerPlugin.getDefault().getPreferenceStore(); store.setDefault(PREF_FOLDING_ENABLED, true); store.setDefault(PREF_AUTOFOLD_COMMENTS, false); + store.setDefault(PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS, false); store.setDefault(PREF_AUTOFOLD_REGIONS, false); store.setDefault(PREF_AUTOFOLD_IMPORT_STATEMENTS, false); } @@ -47,32 +49,61 @@ public FoldingPreferencePage() { @Override public void createFieldEditors() { - Composite parent = getFieldEditorParent(); + final Composite parent = getFieldEditorParent(); - // Add check boxes - addField(new BooleanFieldEditor( // + /* + * check box to globally enable/disable folding + */ + final var foldingEnabled = new BooleanFieldEditor( // PREF_FOLDING_ENABLED, // "Enable folding", //$NON-NLS-1$ - parent)); + parent); + addField(foldingEnabled); - // Add a label before the field editors final var label = new Label(parent, SWT.NONE); - label.setText("Initially fold these elements:"); //$NON-NLS-1$ + label.setText("\nInitially fold these elements:"); //$NON-NLS-1$ label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false)); - // Add check boxes - addField(new BooleanFieldEditor( // + /* + * check boxes to control auto-folding + */ + final var autoFoldComments = new BooleanFieldEditor( // PREF_AUTOFOLD_COMMENTS, // "Comments", //$NON-NLS-1$ - parent)); - addField(new BooleanFieldEditor( // - PREF_AUTOFOLD_COMMENTS, // + parent); + addField(autoFoldComments); + + final var autoFoldLicenseHeaders = new BooleanFieldEditor( // + PREF_AUTOFOLD_LICENSE_HEADERS_COMMENTS, // + "License Header Comments", //$NON-NLS-1$ + parent); + addField(autoFoldLicenseHeaders); + + final var autoFoldRegions = new BooleanFieldEditor( // + PREF_AUTOFOLD_REGIONS, // "Folding Regions", //$NON-NLS-1$ - parent)); - addField(new BooleanFieldEditor( // + parent); + addField(autoFoldRegions); + + final var autoFoldImports = new BooleanFieldEditor( // PREF_AUTOFOLD_IMPORT_STATEMENTS, // "Import statements", //$NON-NLS-1$ - parent)); + parent); + addField(autoFoldImports); + + /* + * update editor states + */ + final Runnable updateEditorStates = () -> { + autoFoldComments.setEnabled(foldingEnabled.getBooleanValue(), parent); + autoFoldLicenseHeaders.setEnabled(foldingEnabled.getBooleanValue() && !autoFoldComments.getBooleanValue(), + parent); + autoFoldRegions.setEnabled(foldingEnabled.getBooleanValue(), parent); + autoFoldImports.setEnabled(foldingEnabled.getBooleanValue(), parent); + }; + foldingEnabled.getDescriptionControl(parent).addListener(SWT.Selection, event -> updateEditorStates.run()); + autoFoldComments.getDescriptionControl(parent).addListener(SWT.Selection, event -> updateEditorStates.run()); + UI.getDisplay().asyncExec(updateEditorStates); } @Override