From 62c792ecba2353705cfe1d7fd9a96ff603e9040f Mon Sep 17 00:00:00 2001 From: Elsa Zacharia Date: Fri, 8 May 2026 16:10:42 +0530 Subject: [PATCH 01/19] Remove unnecessary Apply and close button in property dialog This commit retains only the cancel button and eliminates the Apply and close button in the property dialog which is unnecessary in the current context there. There is only a description provided in the property dialog and there is no event that needs an apply option. Fixes: https://github.com/eclipse-equinox/p2/issues/1049 --- .../ui/internal/dialogs/PropertyDialog.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java index 598111cce77..c420c79d02e 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/dialogs/PropertyDialog.java @@ -17,6 +17,7 @@ import java.util.Iterator; import org.eclipse.core.runtime.Adapters; +import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.preference.IPreferenceNode; import org.eclipse.jface.preference.PreferenceManager; @@ -24,6 +25,7 @@ import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.osgi.util.NLS; +import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; @@ -90,6 +92,20 @@ public static PropertyDialog createDialogOn(Shell shell, final String propertyPa return propertyDialog; } + + @Override + protected void buttonPressed(int buttonId) { + if (buttonId == IDialogConstants.CLOSE_ID) { + cancelPressed(); + return; + } + super.buttonPressed(buttonId); + } + + @Override + protected void createButtonsForButtonBar(Composite parent) { + createButton(parent, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, true); + } @Override protected void addButtonsToHelpControl(Control control) { @@ -162,6 +178,11 @@ protected String getSelectedNodePreference() { return lastPropertyId; } + @Override + public void updateButtons() { + // This function is overridden to remove the Apply and close button + } + /** * Get the name of the selected item preference */ From 75797b1d40ac9b96d15ebe84ccb35d51bd5c0eab Mon Sep 17 00:00:00 2001 From: Christopher Hermann Date: Wed, 6 May 2026 09:38:36 +0200 Subject: [PATCH 02/19] Fix sticky scroll height calculation for variable line heights When emoji or other content affects line heights, sticky scroll was calculating the total height wrong by assuming all lines were the same. Now it gets the actual height for each line instead. Also fixes the height check for limiting visible lines. --- .../stickyscroll/StickyScrollingControl.java | 8 ++++-- .../StickyScrollingControlTest.java | 28 +++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java index 12bb922768e..ed21303fb73 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControl.java @@ -331,7 +331,10 @@ private void calculateAndSetStickyLinesCanvasBounds() { StyledText textWidget= sourceViewer.getTextWidget(); int numberStickyLines= getNumberStickyLines(); - int lineHeight= stickyLineText.getLineHeight() * numberStickyLines; + int lineHeight = 0; + for (int i = 0; i < numberStickyLines; i++) { + lineHeight += stickyLineText.getLineHeight(stickyLineText.getOffsetAtLine(i)); + } int spacingHeight= stickyLineText.getLineSpacing() * (numberStickyLines - 1); int separatorHeight= bottomSeparator.getBounds().height; @@ -450,7 +453,8 @@ private boolean areStickyLinesOutDated(StyledText textWidget) { } private void limitVisibleStickyLinesToTextWidgetHeight(StyledText textWidget) { - int lineHeight= textWidget.getLineHeight() + textWidget.getLineSpacing(); + int topOffset = textWidget.getOffsetAtLine(textWidget.getTopIndex()); + int lineHeight = textWidget.getLineHeight(topOffset) + textWidget.getLineSpacing(); int textWidgetHeight= textWidget.getBounds().height; int visibleLinesInTextWidget= textWidgetHeight / lineHeight; diff --git a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java index babf3580e22..d55ab75f6cb 100644 --- a/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java +++ b/tests/org.eclipse.ui.editors.tests/src/org/eclipse/ui/internal/texteditor/stickyscroll/StickyScrollingControlTest.java @@ -17,6 +17,7 @@ import static org.hamcrest.Matchers.greaterThan; 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 static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -236,6 +237,33 @@ void testStyling() { assertEquals(hoverColor, stickyLineText.getForeground()); } + @Test + void testCanvasBoundsHeightAdjustsForVariableLineHeights() { + sourceViewer.getTextWidget().setBounds(0, 0, 200, 200); + + // Step 1: Set 2 plain-text sticky lines and record canvas height + List plainLines = List.of(new StickyLineStub("line 1", 0), new StickyLineStub("line 2", 1)); + stickyScrollingControl.setStickyLines(plainLines); + Canvas stickyControlCanvas = getStickyControlCanvas(shell); + int heightWithPlainText = stickyControlCanvas.getBounds().height; + + // Step 2: Replace second sticky line with line requiring space + Font largerFont = new Font(Display.getDefault(), + new FontData(shell.getFont().getFontData()[0].getName(), 40, SWT.NORMAL)); + String bigText = "line 2 big"; //$NON-NLS-1$ + StyleRange bigFontRange = new StyleRange(0, bigText.length(), null, null); + bigFontRange.font = largerFont; + List linesWithLargerFont = List.of(new StickyLineStub("line 1", 0), + new StickyLineStub(bigText, 1, new StyleRange[] { bigFontRange })); + stickyScrollingControl.setStickyLines(linesWithLargerFont); + int heightWithLargerFont = getStickyControlCanvas(shell).getBounds().height; + + assertTrue(heightWithLargerFont > heightWithPlainText, + "Canvas height must increase when one sticky line has a larger font"); //$NON-NLS-1$ + + largerFont.dispose(); + } + @Test void testLayoutStickyLinesCanvasOnResize() { sourceViewer.getTextWidget().setBounds(0, 0, 200, 200); From 8ad0c90f83e9603b627e3fbeb9aaf9390d4fd89c Mon Sep 17 00:00:00 2001 From: Eclipse Platform Bot Date: Mon, 1 Jun 2026 06:24:36 +0000 Subject: [PATCH 03/19] Version bump(s) for 4.41 stream --- bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF b/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF index 8e375d7187a..8cdb3e39545 100644 --- a/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.ui.editors/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.ui.editors; singleton:=true -Bundle-Version: 3.22.0.qualifier +Bundle-Version: 3.22.100.qualifier Bundle-Activator: org.eclipse.ui.internal.editors.text.EditorsPlugin Bundle-ActivationPolicy: lazy Bundle-Vendor: %providerName From 55391d7be3e21e418c4cae9fe36ecb7422323beb Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 7 May 2026 07:47:09 +0200 Subject: [PATCH 04/19] Merge AbstractCSSEngine into CSSEngineImpl The CSS core engine had two stacked abstract classes with no concrete core implementation: AbstractCSSEngine (1,113 LOC) and a tiny CSSEngineImpl (95 LOC) that only added parser-factory wiring, property-handler registration helpers, and the boolean value converter. Inline the small subclass into the base, keep the name CSSEngineImpl since that is the type SWT engine subclasses and tests already reference, and make makeCSSParser concrete (it was abstract solely so CSSEngineImpl could supply its single implementation). The class remains abstract because the CSSEngine.reapply() method has no sensible default in the core bundle. Update ParserTestUtil's parseCssWithoutImports cast and rename AbstractCSSEngineTest to CSSEngineImplTest in the same pass to keep test class names aligned with production class names. The test's anonymous subclass no longer needs to override makeCSSParser since it is now concrete on the base. All bundles internal (x-friends only); no API surface changed. Contributes to #3980 --- .../core/impl/engine/AbstractCSSEngine.java | 1122 ----------------- .../css/core/impl/engine/CSSEngineImpl.java | 1113 +++++++++++++++- ...EngineTest.java => CSSEngineImplTest.java} | 12 +- .../ui/tests/css/core/CssCoreTestSuite.java | 4 +- .../tests/css/core/util/ParserTestUtil.java | 4 +- 5 files changed, 1102 insertions(+), 1153 deletions(-) delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java rename tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/{AbstractCSSEngineTest.java => CSSEngineImplTest.java} (84%) diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java deleted file mode 100644 index 66823482a9c..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/AbstractCSSEngine.java +++ /dev/null @@ -1,1122 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2020 Angelo Zerr and others. - * - * 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 - * - * Contributors: - * Angelo Zerr - initial API and implementation - * IBM Corporation - ongoing development - * Red Hat Inc. (mistria) - Fixes suggested by FindBugs - * Red Hat Inc. (mistria) - Bug 413348: fix stream leak - * Lars Vogel - Bug 428715 - * Brian de Alwis (MTI) - Performance tweaks (Bug 430829) - * Dirk Fauth - Bug 479896 - * Patrik Suzzi - Bug 500402 - * Daniel Raap - Bug 511836 - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.impl.engine; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.io.Reader; -import java.io.StringReader; -import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.BiConsumer; -import org.eclipse.core.runtime.FileLocator; -import org.eclipse.core.runtime.IPath; -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.eclipse.e4.ui.css.core.dom.ChildVisibilityAwareElement; -import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; -import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; -import org.eclipse.e4.ui.css.core.dom.IElementProvider; -import org.eclipse.e4.ui.css.core.dom.IStreamingNodeList; -import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyCompositeHandler; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2Delegate; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandlerProvider; -import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; -import org.eclipse.e4.ui.css.core.engine.CSSElementContext; -import org.eclipse.e4.ui.css.core.engine.CSSEngine; -import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler; -import org.eclipse.e4.ui.css.core.exceptions.UnsupportedPropertyException; -import org.eclipse.e4.ui.css.core.impl.dom.CSSRuleListImpl; -import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl; -import org.eclipse.e4.ui.css.core.impl.dom.DocumentCSSImpl; -import org.eclipse.e4.ui.css.core.impl.dom.ViewCSSImpl; -import org.eclipse.e4.ui.css.core.impl.sac.ExtendedSelector; -import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; -import org.eclipse.e4.ui.css.core.resources.ResourceRegistryKeyFactory; -import org.eclipse.e4.ui.css.core.util.impl.resources.ResourcesLocatorManager; -import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; -import org.eclipse.e4.ui.css.core.utils.StringUtils; -import org.w3c.css.sac.AttributeCondition; -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.DescendantSelector; -import org.w3c.css.sac.InputSource; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; -import org.w3c.dom.Element; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; -import org.w3c.dom.css.CSSImportRule; -import org.w3c.dom.css.CSSRule; -import org.w3c.dom.css.CSSRuleList; -import org.w3c.dom.css.CSSStyleDeclaration; -import org.w3c.dom.css.CSSStyleSheet; -import org.w3c.dom.css.CSSValue; -import org.w3c.dom.css.DocumentCSS; -import org.w3c.dom.css.ViewCSS; -import org.w3c.dom.stylesheets.StyleSheet; - -/** - * Abstract CSS Engine manage style sheet parsing and store the - * {@link CSSStyleSheet} into {@link DocumentCSS}. - * - * To apply styles, call the {@link #applyStyles(Object, boolean, boolean)} - * method. This method check if {@link ICSSPropertyHandler} is registered for - * apply the CSS property. - * - * @version 1.0.0 - * @author Angelo ZERR - */ -public abstract class AbstractCSSEngine implements CSSEngine { - - /** - * Archives are deliberately identified by exclamation mark in URLs - */ - private static final String ARCHIVE_IDENTIFIER = "!"; - - /** - * Default {@link IResourcesLocatorManager} used to get InputStream, Reader - * resource like Image. - */ - private static final IResourcesLocatorManager defaultResourcesLocatorManager = ResourcesLocatorManager.INSTANCE; - - /** - * w3c {@link DocumentCSS}. - */ - private final ExtendedDocumentCSS documentCSS; - - /** - * w3c {@link ViewCSS}. - */ - private final ViewCSS viewCSS; - - /** - * {@link IElementProvider} used to retrieve w3c Element linked to the - * widget. - */ - private IElementProvider elementProvider; - - protected boolean computeDefaultStyle = false; - - private Map elementsContext = null; - - /** - * CSS Error Handler to intercept error while parsing, applying styles. - */ - private CSSErrorHandler errorHandler; - - private IResourcesLocatorManager resourcesLocatorManager; - - private IResourcesRegistry resourcesRegistry; - - /** - * An ordered list of ICSSPropertyHandlerProvider - */ - protected List propertyHandlerProviders = new ArrayList<>(); - // for performance hold a map of handlers to singleton list - private final Map> propertyHandler2InstanceMap = new HashMap<>(); - - private Map currentCSSPropertiesApplied; - - private boolean throwError; - - private Map valueConverters = null; - - private int parseImport; - - private ResourceRegistryKeyFactory keyFactory; - - public AbstractCSSEngine() { - this(new DocumentCSSImpl()); - } - - public AbstractCSSEngine(ExtendedDocumentCSS documentCSS) { - this.documentCSS = documentCSS; - this.viewCSS = new ViewCSSImpl(documentCSS); - keyFactory = new ResourceRegistryKeyFactory(); - } - - /*--------------- Parse style sheet -----------------*/ - - @Override - public StyleSheet parseStyleSheet(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parseStyleSheet(source); - } - - @Override - public StyleSheet parseStyleSheet(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parseStyleSheet(source); - } - - @Override - public StyleSheet parseStyleSheet(InputSource source) throws IOException { - // Check that CharacterStream or ByteStream is not null - checkInputSource(source); - CSSParser parser = makeCSSParser(); - CSSStyleSheet styleSheet = parser.parseStyleSheet(source); - - CSSRuleList rules = styleSheet.getCssRules(); - int length = rules.getLength(); - CSSRuleListImpl masterList = new CSSRuleListImpl(); - int counter; - for (counter = 0; counter < length; counter++) { - CSSRule rule = rules.item(counter); - if (rule.getType() != CSSRule.IMPORT_RULE) { - break; - } - // processing an import CSS - CSSImportRule importRule = (CSSImportRule) rule; - URL url = null; - if (importRule.getHref().startsWith("platform")) { - url = FileLocator.resolve(new URL(importRule.getHref())); - } else { - IPath p = IPath.fromOSString(source.getURI()); - IPath trim = p.removeLastSegments(1); - boolean isArchive = source.getURI().contains(ARCHIVE_IDENTIFIER); - url = FileLocator - .resolve(new URL(trim.addTrailingSeparator().toString() + ((CSSImportRule) rule).getHref())); - File testFile = new File(url.getFile()); - if (!isArchive&&!testFile.exists()) { - // look in platform default - String path = getResourcesLocatorManager().resolve((importRule).getHref()); - testFile = new File(new URL(path).getFile()); - if (testFile.exists()) { - url = new URL(path); - } - } - } - try (InputStream stream = url.openStream()) { - InputSource tempStream = new InputSource(); - tempStream.setURI(url.toString()); - tempStream.setByteStream(stream); - parseImport++; - try { - styleSheet = (CSSStyleSheet) this.parseStyleSheet(tempStream); - } finally { - parseImport--; - } - CSSRuleList tempRules = styleSheet.getCssRules(); - for (int j = 0; j < tempRules.getLength(); j++) { - masterList.add(tempRules.item(j)); - } - } - } - - // add remaining non import rules - for (int i = counter; i < length; i++) { - masterList.add(rules.item(i)); - } - - // final stylesheet - CSSStyleSheetImpl s = new CSSStyleSheetImpl(); - s.setRuleList(masterList); - if (parseImport == 0) { - documentCSS.addStyleSheet(s); - } - return s; - } - - private void processNodeList(NodeList nodes, BiConsumer consumer, boolean applyStylesToChildNodes) { - if (nodes instanceof IStreamingNodeList) { - ((IStreamingNodeList) nodes).stream().forEach(child -> { - consumer.accept(child, applyStylesToChildNodes); - }); - } else { - int length = nodes.getLength(); - for (int k = 0; k < length; k++) { - consumer.accept(nodes.item(k), applyStylesToChildNodes); - } - } - } - - /** - * Return true if source is valid and false otherwise. - */ - private void checkInputSource(InputSource source) throws IOException { - Reader reader = source.getCharacterStream(); - InputStream stream = source.getByteStream(); - if (reader == null && stream == null) { - throw new IOException( - "CharacterStream or ByteStream cannot be null for the InputSource."); - } - } - - /*--------------- Parse style declaration -----------------*/ - - @Override - public CSSStyleDeclaration parseStyleDeclaration(String style) { - try { - return parseStyleDeclaration(new StringReader(style)); - } catch (IOException e) { - throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ - } - } - - @Override - public CSSStyleDeclaration parseStyleDeclaration(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parseStyleDeclaration(source); - } - - @Override - public CSSStyleDeclaration parseStyleDeclaration(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parseStyleDeclaration(source); - } - - @Override - public CSSStyleDeclaration parseStyleDeclaration(InputSource source) throws IOException { - checkInputSource(source); - CSSParser parser = makeCSSParser(); - return parser.parseStyleDeclaration(source); - } - - /*--------------- Parse CSS Selector -----------------*/ - - @Override - public SelectorList parseSelectors(String selector) { - try { - return parseSelectors(new StringReader(selector)); - } catch (IOException e) { - throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ - } - } - - @Override - public SelectorList parseSelectors(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parseSelectors(source); - } - - @Override - public SelectorList parseSelectors(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parseSelectors(source); - } - - @Override - public SelectorList parseSelectors(InputSource source) throws IOException { - checkInputSource(source); - CSSParser parser = makeCSSParser(); - return parser.parseSelectors(source); - } - - /*--------------- Parse CSS Property Value-----------------*/ - - @Override - public CSSValue parsePropertyValue(Reader reader) throws IOException { - InputSource source = new InputSource(); - source.setCharacterStream(reader); - return parsePropertyValue(source); - } - - @Override - public CSSValue parsePropertyValue(InputStream stream) throws IOException { - InputSource source = new InputSource(); - source.setByteStream(stream); - return parsePropertyValue(source); - } - - @Override - public CSSValue parsePropertyValue(String value) { - try { - return parsePropertyValue(new StringReader(value)); - } catch (IOException e) { - throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ - } - } - - @Override - public CSSValue parsePropertyValue(InputSource source) throws IOException { - checkInputSource(source); - CSSParser parser = makeCSSParser(); - return parser.parsePropertyValue(source); - } - - /*--------------- Apply styles -----------------*/ - - @Override - public void applyStyles(Object element, boolean applyStylesToChildNodes) { - applyStyles(element, applyStylesToChildNodes, computeDefaultStyle); - } - - @Override - public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { - Element elt = getElement(element); - if (elt == null || !isVisible(elt)) { - return; - } - - /* - * Compute new Style to apply. - */ - CSSStyleDeclaration style = viewCSS.getComputedStyle(elt, null); - if (computeDefaultStyle) { - if (applyStylesToChildNodes) { - this.computeDefaultStyle = computeDefaultStyle; - } - /* - * Apply default style. - */ - applyDefaultStyleDeclaration(element, false, style, null); - } - - /* - * Manage static pseudo instances - */ - String[] pseudoInstances = getStaticPseudoInstances(elt); - if (pseudoInstances != null && pseudoInstances.length > 0) { - // there are static pseudo instances defined, loop for it and - // apply styles for each pseudo instance. - for (String pseudoInstance : pseudoInstances) { - CSSStyleDeclaration styleWithPseudoInstance = viewCSS.getComputedStyle(elt, pseudoInstance); - if (computeDefaultStyle) { - /* - * Apply default style for the current pseudo instance. - */ - applyDefaultStyleDeclaration(element, false, styleWithPseudoInstance, pseudoInstance); - } - - if (styleWithPseudoInstance != null) { - CSSRule parentRule = styleWithPseudoInstance.getParentRule(); - if (parentRule instanceof ExtendedCSSRule) { - applyConditionalPseudoStyle((ExtendedCSSRule) parentRule, pseudoInstance, element, styleWithPseudoInstance); - } else { - applyStyleDeclaration(elt, styleWithPseudoInstance, pseudoInstance); - } - } - } - } - - if (style != null) { - applyStyleDeclaration(elt, style, null); - } - try { - // Apply inline style - applyInlineStyle(elt, false); - } catch (Exception e) { - handleExceptions(e); - } - - if (applyStylesToChildNodes) { - /* - * Style all children recursive. - */ - NodeList nodes = elt instanceof ChildVisibilityAwareElement c - ? c.getVisibleChildNodes() - : elt.getChildNodes(); - if (nodes != null) { - processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); - onStylesAppliedToChildNodes(elt, nodes); - } - } - } - - /** - * Allow the CSS engine to skip particular elements if they are not visible. - * Elements need to be restyled when they become visible. - * - * @return true if the element is visible, false if not visible. - */ - protected boolean isVisible(Element elt) { - Node parentNode = elt.getParentNode(); - if (parentNode instanceof ChildVisibilityAwareElement) { - NodeList l = ((ChildVisibilityAwareElement) parentNode).getVisibleChildNodes(); - if (l != null) { - if (l instanceof IStreamingNodeList) { - return ((IStreamingNodeList) l).stream().anyMatch(node -> node == elt); - } else { - int length = l.getLength(); - for (int i = 0; i < length; i++) { - if (l.item(i) == elt) { - return true; - } - } - } - } - return false; - } - return true; - } - - private void applyConditionalPseudoStyle(ExtendedCSSRule parentRule, String pseudoInstance, Object element, CSSStyleDeclaration styleWithPseudoInstance) { - SelectorList selectorList = parentRule.getSelectorList(); - for (int j = 0; j < selectorList.getLength(); j++) { - Selector item = selectorList.item(j); - // search for conditional selectors - ConditionalSelector conditional = null; - if (item instanceof ConditionalSelector) { - conditional = (ConditionalSelector) item; - } else if (item instanceof DescendantSelector) { - if (((DescendantSelector) item).getSimpleSelector() instanceof ConditionalSelector) { - conditional = (ConditionalSelector) ((DescendantSelector) item).getSimpleSelector(); - } else if (((DescendantSelector) item).getAncestorSelector() instanceof ConditionalSelector) { - conditional = (ConditionalSelector) ((DescendantSelector) item).getAncestorSelector(); - } - } - if (conditional != null) { - Condition condition = conditional.getCondition(); - // we're only interested in attribute selector conditions - AttributeCondition attr = null; - if (condition instanceof AttributeCondition) { - attr = (AttributeCondition) condition; - } else if (condition instanceof CombinatorCondition) { - if (((CombinatorCondition) condition).getSecondCondition() instanceof AttributeCondition) { - attr = (AttributeCondition) ((CombinatorCondition) condition).getSecondCondition(); - } else if (((CombinatorCondition) condition).getFirstCondition() instanceof AttributeCondition) { - attr = (AttributeCondition) ((CombinatorCondition) condition).getFirstCondition(); - } - } - if (attr != null) { - String value = attr.getValue(); - if (value.equals(pseudoInstance)) { - // if we match the pseudo, apply the style - applyStyleDeclaration(element, styleWithPseudoInstance, pseudoInstance); - return; - } - } - } - } - } - - protected String[] getStaticPseudoInstances(Element element) { - if (element instanceof CSSStylableElement stylableElement) { - return stylableElement.getStaticPseudoInstances(); - } - return null; - } - - /** - * Callback method called when styles applied of nodes - * children of the element. - */ - protected void onStylesAppliedToChildNodes(Element element, NodeList nodes) { - if (element instanceof CSSStylableElement) { - ((CSSStylableElement) element).onStylesApplied(nodes); - } - } - - /*--------------- Apply style declaration -----------------*/ - - @Override - public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, String pseudo) { - // Apply style - boolean avoidanceCacheInstalled = currentCSSPropertiesApplied == null; - if (avoidanceCacheInstalled) { - currentCSSPropertiesApplied = new HashMap<>(); - } - List handlers2 = Collections.emptyList(); - for (int i = 0; i < style.getLength(); i++) { - String property = style.item(i); - CSSValue value = style.getPropertyCSSValue(property); - try { - ICSSPropertyHandler handler = this.applyCSSProperty(element, property, value, pseudo); - ICSSPropertyHandler2 propertyHandler2 = null; - if (handler instanceof ICSSPropertyHandler2) { - propertyHandler2 = (ICSSPropertyHandler2) handler; - } else if (handler instanceof ICSSPropertyHandler2Delegate) { - propertyHandler2 = ((ICSSPropertyHandler2Delegate) handler).getCSSPropertyHandler2(); - } - if (propertyHandler2 != null) { - switch (handlers2.size()) { - case 0: - handlers2 = propertyHandler2InstanceMap.computeIfAbsent(propertyHandler2, - Collections::singletonList); - break; - case 1: - handlers2 = new ArrayList<>(handlers2); - handlers2.add(propertyHandler2); - break; - default: - if (!handlers2.contains(propertyHandler2)) { - handlers2.add(propertyHandler2); - } - } - } - } catch (Exception e) { - if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { - handleExceptions(e); - } - } - } - for (ICSSPropertyHandler2 handler2 : handlers2) { - try { - handler2.onAllCSSPropertiesApplyed(element, this, pseudo); - } catch (Exception e) { - handleExceptions(e); - } - } - if (avoidanceCacheInstalled) { - currentCSSPropertiesApplied = null; - } - - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, - Reader reader) throws IOException { - CSSStyleDeclaration style = parseStyleDeclaration(reader); - this.applyStyleDeclaration(node, style, null); - return style; - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputStream stream) throws IOException { - CSSStyleDeclaration style = parseStyleDeclaration(stream); - this.applyStyleDeclaration(node, style, null); - return style; - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputSource source) throws IOException { - CSSStyleDeclaration style = parseStyleDeclaration(source); - this.applyStyleDeclaration(node, style, null); - return style; - } - - @Override - public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, String style) throws IOException { - CSSStyleDeclaration styleDeclaration = parseStyleDeclaration(style); - this.applyStyleDeclaration(node, styleDeclaration, null); - return styleDeclaration; - } - - /*--------------- Apply inline style -----------------*/ - - @Override - public void applyInlineStyle(Object node, boolean applyStylesToChildNodes) { - Element elt = getElement(node); - if (elt != null) { - if (elt instanceof CSSStylableElement stylableElement) { - String style = stylableElement.getCSSStyle(); - if (style != null && style.length() > 0) { - try { - parseAndApplyStyleDeclaration(stylableElement.getNativeWidget(), style); - } catch (IOException e) { - handleExceptions(e); - } - } - } - if (applyStylesToChildNodes) { - /* - * Style all children recursive. - */ - NodeList nodes = elt.getChildNodes(); - if (nodes != null) { - processNodeList(nodes, this::applyInlineStyle, applyStylesToChildNodes); - } - } - } - } - - /*--------------- Initial Style -----------------*/ - - @Override - public CSSStyleDeclaration getDefaultStyleDeclaration(Object element, String pseudoE) { - return getDefaultStyleDeclaration(element, null, pseudoE); - } - - public CSSStyleDeclaration getDefaultStyleDeclaration(Object widget, CSSStyleDeclaration newStyle, String pseudoE) { - CSSStyleDeclaration style = null; - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - try { - style = provider.getDefaultCSSStyleDeclaration(this, widget, newStyle, pseudoE); - } catch (Exception e) { - handleExceptions(e); - } - } - return style; - } - - @Override - public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes) { - applyDefaultStyleDeclaration(element, applyStylesToChildNodes, null, null); - } - - public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes, - CSSStyleDeclaration newStyle, String pseudoE) { - // Initial styles must be computed or applied - Element elt = getElement(element); - if (elt != null) { - if (elt instanceof CSSStylableElement stylableElement) { - CSSStyleDeclaration oldDefaultStyleDeclaration = stylableElement.getDefaultStyleDeclaration(pseudoE); - CSSStyleDeclaration defaultStyleDeclaration = getDefaultStyleDeclaration( - element, newStyle, pseudoE); - if (oldDefaultStyleDeclaration != null) { - // Second apply styles, apply the initial style - // before apply the new style - try { - throwError = false; - applyStyleDeclaration(element, defaultStyleDeclaration, pseudoE); - } finally { - throwError = true; - } - } - } - if (applyStylesToChildNodes) { - /* - * Style all children recursive. - */ - NodeList nodes = elt.getChildNodes(); - if (nodes != null) { - processNodeList(nodes, this::applyDefaultStyleDeclaration, applyStylesToChildNodes); - onStylesAppliedToChildNodes(elt, nodes); - } - } - } - } - - /** - * Delegates the handle method. - * - * @param element - * may be a widget or a node or some object - */ - @Override - public ICSSPropertyHandler applyCSSProperty(Object element, String property, CSSValue value, String pseudo) - throws Exception { - if (currentCSSPropertiesApplied != null && currentCSSPropertiesApplied.containsKey(property)) { - // CSS Property was already applied, ignore it. - return null; - } - - element = getElement(element); // in case we're passed a node - if ("inherit".equals(value.getCssText())) { - // go to parent node - Element actualElement = (Element) element; - Node parentNode = actualElement.getParentNode(); - // get CSS property value - String parentValueString = retrieveCSSProperty(parentNode, property, pseudo); - // and convert it to a CSS value, overriding the "inherit" setting - // with the parent value - value = parsePropertyValue(parentValueString); - } - - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - Collection handlers = provider.getCSSPropertyHandlers(element, property); - if (handlers == null) { - continue; - } - for (ICSSPropertyHandler handler : handlers) { - try { - boolean result = handler.applyCSSProperty(element, property, value, pseudo, this); - if (result) { - // Add CSS Property to flag that this CSS Property was - // applied. - if (currentCSSPropertiesApplied != null) { - currentCSSPropertiesApplied.put(property, property); - } - return handler; - } - } catch (Exception e) { - if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { - handleExceptions(e); - } - } - } - } - - return null; - } - - @Override - public String retrieveCSSProperty(Object element, String property, String pseudo) { - try { - element = getElement(element); // in case we're passed a node - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - Collection handlers = provider.getCSSPropertyHandlers(element, property); - if (handlers == null) { - continue; - } - for (ICSSPropertyHandler handler : handlers) { - String value = handler.retrieveCSSProperty(element, property, pseudo, this); - if (!StringUtils.isEmpty(value)) { - return value; - } - } - } - } catch (Exception e) { - handleExceptions(e); - } - return null; - } - - @Override - public String[] getCSSCompositePropertiesNames(String property) { - try { - Collection handlers = getCSSPropertyHandlers(property); - if (handlers == null) { - return null; - } - for (ICSSPropertyHandler handler : handlers) { - if (handler instanceof ICSSPropertyCompositeHandler compositeHandler) { - if (compositeHandler.isCSSPropertyComposite(property)) { - return compositeHandler.getCSSPropertiesNames(property); - } - } - } - } catch (Exception e) { - handleExceptions(e); - } - return null; - } - - protected Collection getCSSPropertyHandlers(String property) throws Exception { - Collection handlers = new ArrayList<>(); - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - Collection h = provider.getCSSPropertyHandlers(property); - if (handlers == null) { - handlers = h; - } else { - handlers = new ArrayList<>(handlers); - handlers.addAll(h); - } - } - return handlers; - } - - /** - * Return the set of property names and handlers for the provided node. - * - * @return the property names and handlers - */ - @Override - public Collection getCSSProperties(Object element) { - Set properties = new HashSet<>(); - for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { - properties.addAll(provider.getCSSProperties(element)); - } - return properties; - } - - /*--------------- Dynamic pseudo classes -----------------*/ - - @Override - public IElementProvider getElementProvider() { - return elementProvider; - } - - @Override - public void setElementProvider(IElementProvider elementProvider) { - this.elementProvider = elementProvider; - } - - /** - * Return the w3c Element linked to the Object element. - */ - @Override - public Element getElement(Object element) { - Element elt = null; - if (element == null) { - return elt; - } - CSSElementContext elementContext = getCSSElementContext(element); - if (elementContext != null) { - if (!elementContext.elementMustBeRefreshed(elementProvider)) { - return elementContext.getElement(); - } - } - if (element instanceof Element) { - elt = (Element) element; - } else if (elementProvider != null) { - elt = elementProvider.getElement(element, this); - } - if (elt != null) { - if (elementContext == null) { - elementContext = new CSSElementContextImpl(); - Object nativeWidget = getNativeWidget(element); - hookNativeWidget(nativeWidget); - getElementsContext().put(nativeWidget, elementContext); - } - elementContext.setElementProvider(elementProvider); - elementContext.setElement(elt); - if (elt instanceof CSSStylableElement) { - // Initialize CSS stylable element - ((CSSStylableElement)elt).initialize(); - } - - } - return elt; - } - - /** - * Called when an element context is created for a native widget and - * registered with this engine. Subclasses should override and install - * a listener on the widget that will call {@link #handleWidgetDisposed(Object)} - * when the widget is disposed. - *

- * The default implementation of this method does nothing. - *

- * - * @param widget the native widget to hook - */ - protected void hookNativeWidget(Object widget) { - } - - /** - * Called when a widget is disposed. Removes the element context - * from the element contexts map and the widgets map. Overriding - * classes must call the super implementation. - */ - @Override - public void handleWidgetDisposed(Object widget) { - if (elementsContext != null) { - elementsContext.remove(widget); - } - } - - @Override - public CSSElementContext getCSSElementContext(Object element) { - Object o = getNativeWidget(element); - return getElementsContext().get(o); - } - - public Object getNativeWidget(Object element) { - Object o = element; - if (element instanceof CSSStylableElement) { - o = ((CSSStylableElement) o).getNativeWidget(); - } - return o; - } - - protected Map getElementsContext() { - if (elementsContext == null) { - elementsContext = new HashMap<>(); - } - return elementsContext; - } - - @Override - public boolean matches(Selector selector, Object element, String pseudoElt) { - Element elt = getElement(element); - if (elt == null) { - return false; - } - if (selector instanceof ExtendedSelector extendedSelector) { - return extendedSelector.match(elt, pseudoElt); - } else { - // TODO : selector is not batik ExtendedSelector, - // Manage this case... - } - return false; - } - - /*--------------- Error Handler -----------------*/ - - /** - * Handle exceptions thrown while parsing, applying styles. By default this - * method call CSS Error Handler if it is initialized. - */ - @Override - public void handleExceptions(Exception e) { - if (errorHandler != null) { - errorHandler.error(e); - } - } - - @Override - public CSSErrorHandler getErrorHandler() { - return errorHandler; - } - - /** - * Set the CSS Error Handler to manage exception. - */ - @Override - public void setErrorHandler(CSSErrorHandler errorHandler) { - this.errorHandler = errorHandler; - } - - /*--------------- Resources Locator Manager -----------------*/ - - @Override - public IResourcesLocatorManager getResourcesLocatorManager() { - if (resourcesLocatorManager == null) { - return defaultResourcesLocatorManager; - } - return resourcesLocatorManager; - } - - @Override - public void setResourcesLocatorManager( - IResourcesLocatorManager resourcesLocatorManager) { - this.resourcesLocatorManager = resourcesLocatorManager; - } - - /*--------------- Document/View CSS -----------------*/ - - @Override - public DocumentCSS getDocumentCSS() { - return documentCSS; - } - - @Override - public ViewCSS getViewCSS() { - return viewCSS; - } - - @Override - public void dispose() { - reset(); - // Call dispose for each CSSStylableElement which was registered - Collection contexts = elementsContext.values(); - for (CSSElementContext context : contexts) { - Element element = context.getElement(); - if (element instanceof CSSStylableElement) { - ((CSSStylableElement) element).dispose(); - } - } - // FIXME: should dispose element provider and the property handler - // providers - elementsContext = null; - if (resourcesRegistry != null) { - resourcesRegistry.dispose(); - } - } - - @Override - public void reset() { - // Remove All Style Sheets - documentCSS.removeAllStyleSheets(); - } - - /*--------------- Resources Registry -----------------*/ - - @Override - public IResourcesRegistry getResourcesRegistry() { - return resourcesRegistry; - } - - @Override - public void setResourcesRegistry(IResourcesRegistry resourcesRegistry) { - this.resourcesRegistry = resourcesRegistry; - } - - public void registerCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { - propertyHandlerProviders.add(handlerProvider); - } - - public void unregisterCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { - propertyHandlerProviders.remove(handlerProvider); - } - - /*--------------- CSS Value Converter -----------------*/ - - @Override - public void registerCSSValueConverter(ICSSValueConverter converter) { - if (valueConverters == null) { - valueConverters = new HashMap<>(); - } - valueConverters.put(converter.getToType(), converter); - } - - @Override - public void unregisterCSSValueConverter(ICSSValueConverter converter) { - if (valueConverters == null) { - return; - } - valueConverters.remove(converter); - } - - @Override - public ICSSValueConverter getCSSValueConverter(Object toType) { - if (valueConverters != null) { - return valueConverters.get(toType); - } - return null; - } - - @Override - public Object convert(CSSValue value, Object toType, Object context) throws Exception { - if ("unset".equals(value.getCssText())) { - return null; - } - Object key = keyFactory.createKey(value); - Object newValue = getResource(toType, key); - - if (newValue == null) { - ICSSValueConverter converter = getCSSValueConverter(toType); - if (converter != null) { - newValue = converter.convert(value, this, context); - // cache it - registerResource(toType, key, newValue); - } - } - return newValue; - } - - private Object getResource(Object toType, Object key) { - if (key != null && getResourcesRegistry() != null) { - return getResourcesRegistry().getResource(toType, key); - } - return null; - } - - private void registerResource(Object toType, Object key, Object resource) { - if (key != null && resource != null && getResourcesRegistry() != null) { - getResourcesRegistry().registerResource(toType, key, resource); - } - } - - @Override - public String convert(Object value, Object toType, Object context) - throws Exception { - if (value == null) { - return null; - } - ICSSValueConverter converter = getCSSValueConverter(toType); - if (converter != null) { - return converter.convert(value, this, context); - } - return null; - } - - /*--------------- Abstract methods -----------------*/ - - /** - * Return instance of CSS Parser. - */ - public abstract CSSParser makeCSSParser(); - - protected void setResourceRegistryKeyFactory(ResourceRegistryKeyFactory keyFactory) { - this.keyFactory = keyFactory; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java index a463e82452d..bfa459895e9 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2015 Angelo Zerr and others. + * Copyright (c) 2008, 2020 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -10,56 +10,1130 @@ * * Contributors: * Angelo Zerr - initial API and implementation - * Brian de Alwis (MTI) - move out registry-specific element provisioning + * IBM Corporation - ongoing development + * Red Hat Inc. (mistria) - Fixes suggested by FindBugs + * Red Hat Inc. (mistria) - Bug 413348: fix stream leak + * Lars Vogel - Bug 428715 + * Brian de Alwis (MTI) - Performance tweaks (Bug 430829) + * Dirk Fauth - Bug 479896 + * Patrik Suzzi - Bug 500402 + * Daniel Raap - Bug 511836 *******************************************************************************/ package org.eclipse.e4.ui.css.core.impl.engine; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; +import java.io.StringReader; +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiConsumer; +import org.eclipse.core.runtime.FileLocator; +import org.eclipse.core.runtime.IPath; +import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; +import org.eclipse.e4.ui.css.core.dom.ChildVisibilityAwareElement; +import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; +import org.eclipse.e4.ui.css.core.dom.IElementProvider; +import org.eclipse.e4.ui.css.core.dom.IStreamingNodeList; import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; import org.eclipse.e4.ui.css.core.dom.parsers.CSSParserFactory; import org.eclipse.e4.ui.css.core.dom.parsers.ICSSParserFactory; +import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyCompositeHandler; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; +import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; +import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2Delegate; +import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandlerProvider; import org.eclipse.e4.ui.css.core.dom.properties.converters.CSSValueBooleanConverterImpl; +import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; import org.eclipse.e4.ui.css.core.dom.properties.providers.CSSPropertyHandlerLazyProviderImpl; import org.eclipse.e4.ui.css.core.dom.properties.providers.CSSPropertyHandlerSimpleProviderImpl; +import org.eclipse.e4.ui.css.core.engine.CSSElementContext; +import org.eclipse.e4.ui.css.core.engine.CSSEngine; +import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler; +import org.eclipse.e4.ui.css.core.exceptions.UnsupportedPropertyException; +import org.eclipse.e4.ui.css.core.impl.dom.CSSRuleListImpl; +import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl; +import org.eclipse.e4.ui.css.core.impl.dom.DocumentCSSImpl; +import org.eclipse.e4.ui.css.core.impl.dom.ViewCSSImpl; import org.eclipse.e4.ui.css.core.impl.sac.CSSConditionFactoryImpl; import org.eclipse.e4.ui.css.core.impl.sac.CSSSelectorFactoryImpl; +import org.eclipse.e4.ui.css.core.impl.sac.ExtendedSelector; +import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; +import org.eclipse.e4.ui.css.core.resources.ResourceRegistryKeyFactory; +import org.eclipse.e4.ui.css.core.util.impl.resources.ResourcesLocatorManager; +import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; +import org.eclipse.e4.ui.css.core.utils.StringUtils; +import org.w3c.css.sac.AttributeCondition; +import org.w3c.css.sac.CombinatorCondition; +import org.w3c.css.sac.Condition; import org.w3c.css.sac.ConditionFactory; +import org.w3c.css.sac.ConditionalSelector; +import org.w3c.css.sac.DescendantSelector; +import org.w3c.css.sac.InputSource; +import org.w3c.css.sac.Selector; +import org.w3c.css.sac.SelectorList; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.css.CSSImportRule; +import org.w3c.dom.css.CSSRule; +import org.w3c.dom.css.CSSRuleList; +import org.w3c.dom.css.CSSStyleDeclaration; +import org.w3c.dom.css.CSSStyleSheet; +import org.w3c.dom.css.CSSValue; +import org.w3c.dom.css.DocumentCSS; +import org.w3c.dom.css.ViewCSS; +import org.w3c.dom.stylesheets.StyleSheet; -public abstract class CSSEngineImpl extends AbstractCSSEngine { +/** + * Abstract CSS Engine manage style sheet parsing and store the + * {@link CSSStyleSheet} into {@link DocumentCSS}. + * + * To apply styles, call the {@link #applyStyles(Object, boolean, boolean)} + * method. This method check if {@link ICSSPropertyHandler} is registered for + * apply the CSS property. + * + * @version 1.0.0 + * @author Angelo ZERR + */ +public abstract class CSSEngineImpl implements CSSEngine { public static final ConditionFactory CONDITIONFACTORY_INSTANCE = new CSSConditionFactoryImpl( null, "class", null, "id"); + /** + * Archives are deliberately identified by exclamation mark in URLs + */ + private static final String ARCHIVE_IDENTIFIER = "!"; + + /** + * Default {@link IResourcesLocatorManager} used to get InputStream, Reader + * resource like Image. + */ + private static final IResourcesLocatorManager defaultResourcesLocatorManager = ResourcesLocatorManager.INSTANCE; + + /** + * w3c {@link DocumentCSS}. + */ + private final ExtendedDocumentCSS documentCSS; + + /** + * w3c {@link ViewCSS}. + */ + private final ViewCSS viewCSS; + + /** + * {@link IElementProvider} used to retrieve w3c Element linked to the + * widget. + */ + private IElementProvider elementProvider; + + protected boolean computeDefaultStyle = false; + + private Map elementsContext = null; + + /** + * CSS Error Handler to intercept error while parsing, applying styles. + */ + private CSSErrorHandler errorHandler; + + private IResourcesLocatorManager resourcesLocatorManager; + + private IResourcesRegistry resourcesRegistry; + + /** + * An ordered list of ICSSPropertyHandlerProvider + */ + protected List propertyHandlerProviders = new ArrayList<>(); + // for performance hold a map of handlers to singleton list + private final Map> propertyHandler2InstanceMap = new HashMap<>(); + + private Map currentCSSPropertiesApplied; + + private boolean throwError; + + private Map valueConverters = null; + + private int parseImport; + + private ResourceRegistryKeyFactory keyFactory; + private CSSPropertyHandlerSimpleProviderImpl handlerProvider; private CSSPropertyHandlerLazyProviderImpl lazyHandlerProvider; public CSSEngineImpl() { - super(); - - // Register SWT Boolean CSSValue Converter - super.registerCSSValueConverter(CSSValueBooleanConverterImpl.INSTANCE); + this(new DocumentCSSImpl()); } public CSSEngineImpl(ExtendedDocumentCSS documentCSS) { - super(documentCSS); - // Register SWT Boolean CSSValue Converter - super.registerCSSValueConverter(CSSValueBooleanConverterImpl.INSTANCE); + this.documentCSS = documentCSS; + this.viewCSS = new ViewCSSImpl(documentCSS); + keyFactory = new ResourceRegistryKeyFactory(); + registerCSSValueConverter(CSSValueBooleanConverterImpl.INSTANCE); + } + + /*--------------- Parse style sheet -----------------*/ + + @Override + public StyleSheet parseStyleSheet(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parseStyleSheet(source); + } + + @Override + public StyleSheet parseStyleSheet(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parseStyleSheet(source); + } + + @Override + public StyleSheet parseStyleSheet(InputSource source) throws IOException { + // Check that CharacterStream or ByteStream is not null + checkInputSource(source); + CSSParser parser = makeCSSParser(); + CSSStyleSheet styleSheet = parser.parseStyleSheet(source); + + CSSRuleList rules = styleSheet.getCssRules(); + int length = rules.getLength(); + CSSRuleListImpl masterList = new CSSRuleListImpl(); + int counter; + for (counter = 0; counter < length; counter++) { + CSSRule rule = rules.item(counter); + if (rule.getType() != CSSRule.IMPORT_RULE) { + break; + } + // processing an import CSS + CSSImportRule importRule = (CSSImportRule) rule; + URL url = null; + if (importRule.getHref().startsWith("platform")) { + url = FileLocator.resolve(new URL(importRule.getHref())); + } else { + IPath p = IPath.fromOSString(source.getURI()); + IPath trim = p.removeLastSegments(1); + boolean isArchive = source.getURI().contains(ARCHIVE_IDENTIFIER); + url = FileLocator + .resolve(new URL(trim.addTrailingSeparator().toString() + ((CSSImportRule) rule).getHref())); + File testFile = new File(url.getFile()); + if (!isArchive&&!testFile.exists()) { + // look in platform default + String path = getResourcesLocatorManager().resolve((importRule).getHref()); + testFile = new File(new URL(path).getFile()); + if (testFile.exists()) { + url = new URL(path); + } + } + } + try (InputStream stream = url.openStream()) { + InputSource tempStream = new InputSource(); + tempStream.setURI(url.toString()); + tempStream.setByteStream(stream); + parseImport++; + try { + styleSheet = (CSSStyleSheet) this.parseStyleSheet(tempStream); + } finally { + parseImport--; + } + CSSRuleList tempRules = styleSheet.getCssRules(); + for (int j = 0; j < tempRules.getLength(); j++) { + masterList.add(tempRules.item(j)); + } + } + } + + // add remaining non import rules + for (int i = counter; i < length; i++) { + masterList.add(rules.item(i)); + } + + // final stylesheet + CSSStyleSheetImpl s = new CSSStyleSheetImpl(); + s.setRuleList(masterList); + if (parseImport == 0) { + documentCSS.addStyleSheet(s); + } + return s; + } + + private void processNodeList(NodeList nodes, BiConsumer consumer, boolean applyStylesToChildNodes) { + if (nodes instanceof IStreamingNodeList) { + ((IStreamingNodeList) nodes).stream().forEach(child -> { + consumer.accept(child, applyStylesToChildNodes); + }); + } else { + int length = nodes.getLength(); + for (int k = 0; k < length; k++) { + consumer.accept(nodes.item(k), applyStylesToChildNodes); + } + } + } + + /** + * Return true if source is valid and false otherwise. + */ + private void checkInputSource(InputSource source) throws IOException { + Reader reader = source.getCharacterStream(); + InputStream stream = source.getByteStream(); + if (reader == null && stream == null) { + throw new IOException( + "CharacterStream or ByteStream cannot be null for the InputSource."); + } + } + + /*--------------- Parse style declaration -----------------*/ + + @Override + public CSSStyleDeclaration parseStyleDeclaration(String style) { + try { + return parseStyleDeclaration(new StringReader(style)); + } catch (IOException e) { + throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ + } + } + + @Override + public CSSStyleDeclaration parseStyleDeclaration(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parseStyleDeclaration(source); + } + + @Override + public CSSStyleDeclaration parseStyleDeclaration(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parseStyleDeclaration(source); + } + + @Override + public CSSStyleDeclaration parseStyleDeclaration(InputSource source) throws IOException { + checkInputSource(source); + CSSParser parser = makeCSSParser(); + return parser.parseStyleDeclaration(source); + } + + /*--------------- Parse CSS Selector -----------------*/ + + @Override + public SelectorList parseSelectors(String selector) { + try { + return parseSelectors(new StringReader(selector)); + } catch (IOException e) { + throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ + } + } + + @Override + public SelectorList parseSelectors(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parseSelectors(source); + } + + @Override + public SelectorList parseSelectors(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parseSelectors(source); + } + + @Override + public SelectorList parseSelectors(InputSource source) throws IOException { + checkInputSource(source); + CSSParser parser = makeCSSParser(); + return parser.parseSelectors(source); + } + + /*--------------- Parse CSS Property Value-----------------*/ + + @Override + public CSSValue parsePropertyValue(Reader reader) throws IOException { + InputSource source = new InputSource(); + source.setCharacterStream(reader); + return parsePropertyValue(source); } @Override + public CSSValue parsePropertyValue(InputStream stream) throws IOException { + InputSource source = new InputSource(); + source.setByteStream(stream); + return parsePropertyValue(source); + } + + @Override + public CSSValue parsePropertyValue(String value) { + try { + return parsePropertyValue(new StringReader(value)); + } catch (IOException e) { + throw new IllegalStateException("StringReader cannot throw IOException", e); //$NON-NLS-1$ + } + } + + @Override + public CSSValue parsePropertyValue(InputSource source) throws IOException { + checkInputSource(source); + CSSParser parser = makeCSSParser(); + return parser.parsePropertyValue(source); + } + + /*--------------- Apply styles -----------------*/ + + @Override + public void applyStyles(Object element, boolean applyStylesToChildNodes) { + applyStyles(element, applyStylesToChildNodes, computeDefaultStyle); + } + + @Override + public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { + Element elt = getElement(element); + if (elt == null || !isVisible(elt)) { + return; + } + + /* + * Compute new Style to apply. + */ + CSSStyleDeclaration style = viewCSS.getComputedStyle(elt, null); + if (computeDefaultStyle) { + if (applyStylesToChildNodes) { + this.computeDefaultStyle = computeDefaultStyle; + } + /* + * Apply default style. + */ + applyDefaultStyleDeclaration(element, false, style, null); + } + + /* + * Manage static pseudo instances + */ + String[] pseudoInstances = getStaticPseudoInstances(elt); + if (pseudoInstances != null && pseudoInstances.length > 0) { + // there are static pseudo instances defined, loop for it and + // apply styles for each pseudo instance. + for (String pseudoInstance : pseudoInstances) { + CSSStyleDeclaration styleWithPseudoInstance = viewCSS.getComputedStyle(elt, pseudoInstance); + if (computeDefaultStyle) { + /* + * Apply default style for the current pseudo instance. + */ + applyDefaultStyleDeclaration(element, false, styleWithPseudoInstance, pseudoInstance); + } + + if (styleWithPseudoInstance != null) { + CSSRule parentRule = styleWithPseudoInstance.getParentRule(); + if (parentRule instanceof ExtendedCSSRule) { + applyConditionalPseudoStyle((ExtendedCSSRule) parentRule, pseudoInstance, element, styleWithPseudoInstance); + } else { + applyStyleDeclaration(elt, styleWithPseudoInstance, pseudoInstance); + } + } + } + } + + if (style != null) { + applyStyleDeclaration(elt, style, null); + } + try { + // Apply inline style + applyInlineStyle(elt, false); + } catch (Exception e) { + handleExceptions(e); + } + + if (applyStylesToChildNodes) { + /* + * Style all children recursive. + */ + NodeList nodes = elt instanceof ChildVisibilityAwareElement c + ? c.getVisibleChildNodes() + : elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); + onStylesAppliedToChildNodes(elt, nodes); + } + } + } + + /** + * Allow the CSS engine to skip particular elements if they are not visible. + * Elements need to be restyled when they become visible. + * + * @return true if the element is visible, false if not visible. + */ + protected boolean isVisible(Element elt) { + Node parentNode = elt.getParentNode(); + if (parentNode instanceof ChildVisibilityAwareElement) { + NodeList l = ((ChildVisibilityAwareElement) parentNode).getVisibleChildNodes(); + if (l != null) { + if (l instanceof IStreamingNodeList) { + return ((IStreamingNodeList) l).stream().anyMatch(node -> node == elt); + } else { + int length = l.getLength(); + for (int i = 0; i < length; i++) { + if (l.item(i) == elt) { + return true; + } + } + } + } + return false; + } + return true; + } + + private void applyConditionalPseudoStyle(ExtendedCSSRule parentRule, String pseudoInstance, Object element, CSSStyleDeclaration styleWithPseudoInstance) { + SelectorList selectorList = parentRule.getSelectorList(); + for (int j = 0; j < selectorList.getLength(); j++) { + Selector item = selectorList.item(j); + // search for conditional selectors + ConditionalSelector conditional = null; + if (item instanceof ConditionalSelector) { + conditional = (ConditionalSelector) item; + } else if (item instanceof DescendantSelector) { + if (((DescendantSelector) item).getSimpleSelector() instanceof ConditionalSelector) { + conditional = (ConditionalSelector) ((DescendantSelector) item).getSimpleSelector(); + } else if (((DescendantSelector) item).getAncestorSelector() instanceof ConditionalSelector) { + conditional = (ConditionalSelector) ((DescendantSelector) item).getAncestorSelector(); + } + } + if (conditional != null) { + Condition condition = conditional.getCondition(); + // we're only interested in attribute selector conditions + AttributeCondition attr = null; + if (condition instanceof AttributeCondition) { + attr = (AttributeCondition) condition; + } else if (condition instanceof CombinatorCondition) { + if (((CombinatorCondition) condition).getSecondCondition() instanceof AttributeCondition) { + attr = (AttributeCondition) ((CombinatorCondition) condition).getSecondCondition(); + } else if (((CombinatorCondition) condition).getFirstCondition() instanceof AttributeCondition) { + attr = (AttributeCondition) ((CombinatorCondition) condition).getFirstCondition(); + } + } + if (attr != null) { + String value = attr.getValue(); + if (value.equals(pseudoInstance)) { + // if we match the pseudo, apply the style + applyStyleDeclaration(element, styleWithPseudoInstance, pseudoInstance); + return; + } + } + } + } + } + + protected String[] getStaticPseudoInstances(Element element) { + if (element instanceof CSSStylableElement stylableElement) { + return stylableElement.getStaticPseudoInstances(); + } + return null; + } + + /** + * Callback method called when styles applied of nodes + * children of the element. + */ + protected void onStylesAppliedToChildNodes(Element element, NodeList nodes) { + if (element instanceof CSSStylableElement) { + ((CSSStylableElement) element).onStylesApplied(nodes); + } + } + + /*--------------- Apply style declaration -----------------*/ + + @Override + public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, String pseudo) { + // Apply style + boolean avoidanceCacheInstalled = currentCSSPropertiesApplied == null; + if (avoidanceCacheInstalled) { + currentCSSPropertiesApplied = new HashMap<>(); + } + List handlers2 = Collections.emptyList(); + for (int i = 0; i < style.getLength(); i++) { + String property = style.item(i); + CSSValue value = style.getPropertyCSSValue(property); + try { + ICSSPropertyHandler handler = this.applyCSSProperty(element, property, value, pseudo); + ICSSPropertyHandler2 propertyHandler2 = null; + if (handler instanceof ICSSPropertyHandler2) { + propertyHandler2 = (ICSSPropertyHandler2) handler; + } else if (handler instanceof ICSSPropertyHandler2Delegate) { + propertyHandler2 = ((ICSSPropertyHandler2Delegate) handler).getCSSPropertyHandler2(); + } + if (propertyHandler2 != null) { + switch (handlers2.size()) { + case 0: + handlers2 = propertyHandler2InstanceMap.computeIfAbsent(propertyHandler2, + Collections::singletonList); + break; + case 1: + handlers2 = new ArrayList<>(handlers2); + handlers2.add(propertyHandler2); + break; + default: + if (!handlers2.contains(propertyHandler2)) { + handlers2.add(propertyHandler2); + } + } + } + } catch (Exception e) { + if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { + handleExceptions(e); + } + } + } + for (ICSSPropertyHandler2 handler2 : handlers2) { + try { + handler2.onAllCSSPropertiesApplyed(element, this, pseudo); + } catch (Exception e) { + handleExceptions(e); + } + } + if (avoidanceCacheInstalled) { + currentCSSPropertiesApplied = null; + } + + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, + Reader reader) throws IOException { + CSSStyleDeclaration style = parseStyleDeclaration(reader); + this.applyStyleDeclaration(node, style, null); + return style; + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputStream stream) throws IOException { + CSSStyleDeclaration style = parseStyleDeclaration(stream); + this.applyStyleDeclaration(node, style, null); + return style; + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, InputSource source) throws IOException { + CSSStyleDeclaration style = parseStyleDeclaration(source); + this.applyStyleDeclaration(node, style, null); + return style; + } + + @Override + public CSSStyleDeclaration parseAndApplyStyleDeclaration(Object node, String style) throws IOException { + CSSStyleDeclaration styleDeclaration = parseStyleDeclaration(style); + this.applyStyleDeclaration(node, styleDeclaration, null); + return styleDeclaration; + } + + /*--------------- Apply inline style -----------------*/ + + @Override + public void applyInlineStyle(Object node, boolean applyStylesToChildNodes) { + Element elt = getElement(node); + if (elt != null) { + if (elt instanceof CSSStylableElement stylableElement) { + String style = stylableElement.getCSSStyle(); + if (style != null && style.length() > 0) { + try { + parseAndApplyStyleDeclaration(stylableElement.getNativeWidget(), style); + } catch (IOException e) { + handleExceptions(e); + } + } + } + if (applyStylesToChildNodes) { + /* + * Style all children recursive. + */ + NodeList nodes = elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyInlineStyle, applyStylesToChildNodes); + } + } + } + } + + /*--------------- Initial Style -----------------*/ + + @Override + public CSSStyleDeclaration getDefaultStyleDeclaration(Object element, String pseudoE) { + return getDefaultStyleDeclaration(element, null, pseudoE); + } + + public CSSStyleDeclaration getDefaultStyleDeclaration(Object widget, CSSStyleDeclaration newStyle, String pseudoE) { + CSSStyleDeclaration style = null; + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + try { + style = provider.getDefaultCSSStyleDeclaration(this, widget, newStyle, pseudoE); + } catch (Exception e) { + handleExceptions(e); + } + } + return style; + } + + @Override + public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes) { + applyDefaultStyleDeclaration(element, applyStylesToChildNodes, null, null); + } + + public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToChildNodes, + CSSStyleDeclaration newStyle, String pseudoE) { + // Initial styles must be computed or applied + Element elt = getElement(element); + if (elt != null) { + if (elt instanceof CSSStylableElement stylableElement) { + CSSStyleDeclaration oldDefaultStyleDeclaration = stylableElement.getDefaultStyleDeclaration(pseudoE); + CSSStyleDeclaration defaultStyleDeclaration = getDefaultStyleDeclaration( + element, newStyle, pseudoE); + if (oldDefaultStyleDeclaration != null) { + // Second apply styles, apply the initial style + // before apply the new style + try { + throwError = false; + applyStyleDeclaration(element, defaultStyleDeclaration, pseudoE); + } finally { + throwError = true; + } + } + } + if (applyStylesToChildNodes) { + /* + * Style all children recursive. + */ + NodeList nodes = elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyDefaultStyleDeclaration, applyStylesToChildNodes); + onStylesAppliedToChildNodes(elt, nodes); + } + } + } + } + + /** + * Delegates the handle method. + * + * @param element + * may be a widget or a node or some object + */ + @Override + public ICSSPropertyHandler applyCSSProperty(Object element, String property, CSSValue value, String pseudo) + throws Exception { + if (currentCSSPropertiesApplied != null && currentCSSPropertiesApplied.containsKey(property)) { + // CSS Property was already applied, ignore it. + return null; + } + + element = getElement(element); // in case we're passed a node + if ("inherit".equals(value.getCssText())) { + // go to parent node + Element actualElement = (Element) element; + Node parentNode = actualElement.getParentNode(); + // get CSS property value + String parentValueString = retrieveCSSProperty(parentNode, property, pseudo); + // and convert it to a CSS value, overriding the "inherit" setting + // with the parent value + value = parsePropertyValue(parentValueString); + } + + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + Collection handlers = provider.getCSSPropertyHandlers(element, property); + if (handlers == null) { + continue; + } + for (ICSSPropertyHandler handler : handlers) { + try { + boolean result = handler.applyCSSProperty(element, property, value, pseudo, this); + if (result) { + // Add CSS Property to flag that this CSS Property was + // applied. + if (currentCSSPropertiesApplied != null) { + currentCSSPropertiesApplied.put(property, property); + } + return handler; + } + } catch (Exception e) { + if (throwError || (!throwError && !(e instanceof UnsupportedPropertyException))) { + handleExceptions(e); + } + } + } + } + + return null; + } + + @Override + public String retrieveCSSProperty(Object element, String property, String pseudo) { + try { + element = getElement(element); // in case we're passed a node + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + Collection handlers = provider.getCSSPropertyHandlers(element, property); + if (handlers == null) { + continue; + } + for (ICSSPropertyHandler handler : handlers) { + String value = handler.retrieveCSSProperty(element, property, pseudo, this); + if (!StringUtils.isEmpty(value)) { + return value; + } + } + } + } catch (Exception e) { + handleExceptions(e); + } + return null; + } + + @Override + public String[] getCSSCompositePropertiesNames(String property) { + try { + Collection handlers = getCSSPropertyHandlers(property); + if (handlers == null) { + return null; + } + for (ICSSPropertyHandler handler : handlers) { + if (handler instanceof ICSSPropertyCompositeHandler compositeHandler) { + if (compositeHandler.isCSSPropertyComposite(property)) { + return compositeHandler.getCSSPropertiesNames(property); + } + } + } + } catch (Exception e) { + handleExceptions(e); + } + return null; + } + + protected Collection getCSSPropertyHandlers(String property) throws Exception { + Collection handlers = new ArrayList<>(); + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + Collection h = provider.getCSSPropertyHandlers(property); + if (handlers == null) { + handlers = h; + } else { + handlers = new ArrayList<>(handlers); + handlers.addAll(h); + } + } + return handlers; + } + + /** + * Return the set of property names and handlers for the provided node. + * + * @return the property names and handlers + */ + @Override + public Collection getCSSProperties(Object element) { + Set properties = new HashSet<>(); + for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + properties.addAll(provider.getCSSProperties(element)); + } + return properties; + } + + /*--------------- Dynamic pseudo classes -----------------*/ + + @Override + public IElementProvider getElementProvider() { + return elementProvider; + } + + @Override + public void setElementProvider(IElementProvider elementProvider) { + this.elementProvider = elementProvider; + } + + /** + * Return the w3c Element linked to the Object element. + */ + @Override + public Element getElement(Object element) { + Element elt = null; + if (element == null) { + return elt; + } + CSSElementContext elementContext = getCSSElementContext(element); + if (elementContext != null) { + if (!elementContext.elementMustBeRefreshed(elementProvider)) { + return elementContext.getElement(); + } + } + if (element instanceof Element) { + elt = (Element) element; + } else if (elementProvider != null) { + elt = elementProvider.getElement(element, this); + } + if (elt != null) { + if (elementContext == null) { + elementContext = new CSSElementContextImpl(); + Object nativeWidget = getNativeWidget(element); + hookNativeWidget(nativeWidget); + getElementsContext().put(nativeWidget, elementContext); + } + elementContext.setElementProvider(elementProvider); + elementContext.setElement(elt); + if (elt instanceof CSSStylableElement) { + // Initialize CSS stylable element + ((CSSStylableElement)elt).initialize(); + } + + } + return elt; + } + + /** + * Called when an element context is created for a native widget and + * registered with this engine. Subclasses should override and install + * a listener on the widget that will call {@link #handleWidgetDisposed(Object)} + * when the widget is disposed. + *

+ * The default implementation of this method does nothing. + *

+ * + * @param widget the native widget to hook + */ + protected void hookNativeWidget(Object widget) { + } + + /** + * Called when a widget is disposed. Removes the element context + * from the element contexts map and the widgets map. Overriding + * classes must call the super implementation. + */ + @Override + public void handleWidgetDisposed(Object widget) { + if (elementsContext != null) { + elementsContext.remove(widget); + } + } + + @Override + public CSSElementContext getCSSElementContext(Object element) { + Object o = getNativeWidget(element); + return getElementsContext().get(o); + } + + public Object getNativeWidget(Object element) { + Object o = element; + if (element instanceof CSSStylableElement) { + o = ((CSSStylableElement) o).getNativeWidget(); + } + return o; + } + + protected Map getElementsContext() { + if (elementsContext == null) { + elementsContext = new HashMap<>(); + } + return elementsContext; + } + + @Override + public boolean matches(Selector selector, Object element, String pseudoElt) { + Element elt = getElement(element); + if (elt == null) { + return false; + } + if (selector instanceof ExtendedSelector extendedSelector) { + return extendedSelector.match(elt, pseudoElt); + } else { + // TODO : selector is not batik ExtendedSelector, + // Manage this case... + } + return false; + } + + /*--------------- Error Handler -----------------*/ + + /** + * Handle exceptions thrown while parsing, applying styles. By default this + * method call CSS Error Handler if it is initialized. + */ + @Override + public void handleExceptions(Exception e) { + if (errorHandler != null) { + errorHandler.error(e); + } + } + + @Override + public CSSErrorHandler getErrorHandler() { + return errorHandler; + } + + /** + * Set the CSS Error Handler to manage exception. + */ + @Override + public void setErrorHandler(CSSErrorHandler errorHandler) { + this.errorHandler = errorHandler; + } + + /*--------------- Resources Locator Manager -----------------*/ + + @Override + public IResourcesLocatorManager getResourcesLocatorManager() { + if (resourcesLocatorManager == null) { + return defaultResourcesLocatorManager; + } + return resourcesLocatorManager; + } + + @Override + public void setResourcesLocatorManager( + IResourcesLocatorManager resourcesLocatorManager) { + this.resourcesLocatorManager = resourcesLocatorManager; + } + + /*--------------- Document/View CSS -----------------*/ + + @Override + public DocumentCSS getDocumentCSS() { + return documentCSS; + } + + @Override + public ViewCSS getViewCSS() { + return viewCSS; + } + + @Override + public void dispose() { + reset(); + // Call dispose for each CSSStylableElement which was registered + Collection contexts = elementsContext.values(); + for (CSSElementContext context : contexts) { + Element element = context.getElement(); + if (element instanceof CSSStylableElement) { + ((CSSStylableElement) element).dispose(); + } + } + // FIXME: should dispose element provider and the property handler + // providers + elementsContext = null; + if (resourcesRegistry != null) { + resourcesRegistry.dispose(); + } + } + + @Override + public void reset() { + // Remove All Style Sheets + documentCSS.removeAllStyleSheets(); + } + + /*--------------- Resources Registry -----------------*/ + + @Override + public IResourcesRegistry getResourcesRegistry() { + return resourcesRegistry; + } + + @Override + public void setResourcesRegistry(IResourcesRegistry resourcesRegistry) { + this.resourcesRegistry = resourcesRegistry; + } + + public void registerCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { + propertyHandlerProviders.add(handlerProvider); + } + + public void unregisterCSSPropertyHandlerProvider(ICSSPropertyHandlerProvider handlerProvider) { + propertyHandlerProviders.remove(handlerProvider); + } + + /*--------------- CSS Value Converter -----------------*/ + + @Override + public void registerCSSValueConverter(ICSSValueConverter converter) { + if (valueConverters == null) { + valueConverters = new HashMap<>(); + } + valueConverters.put(converter.getToType(), converter); + } + + @Override + public void unregisterCSSValueConverter(ICSSValueConverter converter) { + if (valueConverters == null) { + return; + } + valueConverters.remove(converter); + } + + @Override + public ICSSValueConverter getCSSValueConverter(Object toType) { + if (valueConverters != null) { + return valueConverters.get(toType); + } + return null; + } + + @Override + public Object convert(CSSValue value, Object toType, Object context) throws Exception { + if ("unset".equals(value.getCssText())) { + return null; + } + Object key = keyFactory.createKey(value); + Object newValue = getResource(toType, key); + + if (newValue == null) { + ICSSValueConverter converter = getCSSValueConverter(toType); + if (converter != null) { + newValue = converter.convert(value, this, context); + // cache it + registerResource(toType, key, newValue); + } + } + return newValue; + } + + private Object getResource(Object toType, Object key) { + if (key != null && getResourcesRegistry() != null) { + return getResourcesRegistry().getResource(toType, key); + } + return null; + } + + private void registerResource(Object toType, Object key, Object resource) { + if (key != null && resource != null && getResourcesRegistry() != null) { + getResourcesRegistry().registerResource(toType, key, resource); + } + } + + @Override + public String convert(Object value, Object toType, Object context) + throws Exception { + if (value == null) { + return null; + } + ICSSValueConverter converter = getCSSValueConverter(toType); + if (converter != null) { + return converter.convert(value, this, context); + } + return null; + } + + /** + * Return instance of CSS Parser, configured with the Batik selector + * factory and the platform's class/id condition factory. + */ public CSSParser makeCSSParser() { - // Create CSS Parser ICSSParserFactory factory = CSSParserFactory.newInstance(); CSSParser parser = factory.makeCSSParser(); - - // Register Batik CSS Selector factory. parser.setSelectorFactory(CSSSelectorFactoryImpl.INSTANCE); - - // Register Custom CSS Condition factory. parser.setConditionFactory(CONDITIONFACTORY_INSTANCE); - return parser; } @@ -71,7 +1145,7 @@ public void registerCSSPropertyHandler(Class cl, ICSSPropertyHandler handler) private void initHandlerProviderIfNeed() { if (handlerProvider == null) { handlerProvider = new CSSPropertyHandlerSimpleProviderImpl(); - super.registerCSSPropertyHandlerProvider(handlerProvider); + registerCSSPropertyHandlerProvider(handlerProvider); } } @@ -83,7 +1157,7 @@ public void registerCSSProperty(String propertyName, Class { // throws NPE if parameter is null diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java index 60911cc40ec..63b3ca7cd77 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CssCoreTestSuite.java @@ -15,7 +15,7 @@ *******************************************************************************/ package org.eclipse.e4.ui.tests.css.core; -import org.eclipse.e4.ui.css.core.impl.engine.AbstractCSSEngineTest; +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImplTest; import org.eclipse.e4.ui.tests.css.core.dom.CSSPropertyHandlerProviderTest; import org.eclipse.e4.ui.tests.css.core.parser.CascadeTest; import org.eclipse.e4.ui.tests.css.core.parser.FontFaceRulesTest; @@ -44,7 +44,7 @@ CSSEngineTest.class, ImportTest.class, InheritTest.class, - AbstractCSSEngineTest.class, + CSSEngineImplTest.class, CSSPropertyHandlerProviderTest.class }) @Suite diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java index 21fd0fffcbc..e72b95fd186 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/util/ParserTestUtil.java @@ -19,7 +19,7 @@ import org.eclipse.e4.ui.css.core.dom.parsers.CSSParser; import org.eclipse.e4.ui.css.core.engine.CSSEngine; import org.eclipse.e4.ui.css.core.engine.CSSErrorHandler; -import org.eclipse.e4.ui.css.core.impl.engine.AbstractCSSEngine; +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; import org.eclipse.e4.ui.css.swt.engine.CSSSWTEngineImpl; import org.eclipse.swt.widgets.Display; import org.w3c.css.sac.InputSource; @@ -51,7 +51,7 @@ public static CSSStyleSheet parseCss(String css) */ public static CSSStyleSheet parseCssWithoutImports(String css) throws IOException { - CSSParser parser = ((AbstractCSSEngine) createEngine()).makeCSSParser(); + CSSParser parser = ((CSSEngineImpl) createEngine()).makeCSSParser(); InputSource source = new InputSource(); source.setCharacterStream(new StringReader(css)); return parser.parseStyleSheet(source); From 9f96bf30d446a81b78e68cd8db32e72aa614420a Mon Sep 17 00:00:00 2001 From: Tobias Melcher Date: Tue, 26 May 2026 15:46:14 +0200 Subject: [PATCH 05/19] Allow platform:/plugin/ URIs in templates extension include element The 'file' and 'translations' attributes of the element in the org.eclipse.ui.editors.templates extension point previously only accepted plug-in local file paths, resolved relative to the contributing bundle via FileLocator.find(Bundle, IPath, Map). This change extends ContributionTemplateStore.readIncludedTemplates to also accept platform:/ URIs (e.g. platform:/plugin//), allowing a contributor to reference template files that live in another plug-in. Bundle-relative paths continue to work unchanged; platform:/ URIs are resolved via FileLocator.find(URL). --- .../templates/ContributionTemplateStore.java | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java index 1d9167b0126..eec6ebf5d91 100644 --- a/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java +++ b/bundles/org.eclipse.ui.editors/src/org/eclipse/ui/editors/text/templates/ContributionTemplateStore.java @@ -17,6 +17,9 @@ import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.util.ArrayList; import java.util.Collection; @@ -128,12 +131,12 @@ private void readIncludedTemplates(Collection templates String file= element.getAttribute(FILE); if (file != null) { Bundle plugin = Platform.getBundle(element.getContributor().getName()); - URL url= FileLocator.find(plugin, IPath.fromOSString(file), null); + URL url= resolveResource(plugin, file); if (url != null) { ResourceBundle bundle= null; String translations= element.getAttribute(TRANSLATIONS); if (translations != null) { - URL bundleURL= FileLocator.find(plugin, IPath.fromOSString(translations), null); + URL bundleURL= resolveResource(plugin, translations); if (bundleURL != null) { try (InputStream bundleStream= bundleURL.openStream()) { bundle= new PropertyResourceBundle(bundleStream); @@ -160,6 +163,30 @@ private void readIncludedTemplates(Collection templates } } + /** + * Resolves a resource location to a URL. Supports plain bundle-relative paths + * as well as platform:/plugin/ URIs, which allow referencing files + * contributed by other plug-ins. Other platform:/ schemes (e.g. + * platform:/resource/, platform:/fragment/) are not + * handled here and are treated as bundle-relative paths. + * + * @param plugin the contributing bundle, used for bundle-relative lookups + * @param location the resource location (a bundle-relative path or a + * platform:/plugin/ URI) + * @return the resolved URL, or null if it could not be found + */ + private static URL resolveResource(Bundle plugin, String location) { + if (location.startsWith("platform:/plugin/")) { //$NON-NLS-1$ + try { + return FileLocator.find(new URI(location).toURL()); + } catch (URISyntaxException | MalformedURLException e) { + EditorsPlugin.log(e); + return null; + } + } + return FileLocator.find(plugin, IPath.fromOSString(location), null); + } + /** * Validates a template against the context type registered in the context * type registry. Returns always true if no registry is From edbad229b96637d2b8195d273081802301ba3931 Mon Sep 17 00:00:00 2001 From: Tobias Melcher Date: Fri, 29 May 2026 15:19:18 +0200 Subject: [PATCH 06/19] Fix multiline code mining height alignment with text widget lines The line height of multiline code minings was computed via gc.stringExtent(line).y, which on Windows with Consolas at odd point sizes (e.g. 9, 11, 13) returns a different value than StyledText.getLineHeight(). As a result, the regular text lines drawn below a multiline code mining were shifted by a few pixels and no longer aligned with the rest of the text grid. Use textWidget.getLineHeight() (plus textWidget.getLineSpacing()) in LineHeaderCodeMining.draw, AbstractCodeMining.draw and CodeMiningLineHeaderAnnotation.calculateLineHeight so each code mining line contributes exactly one StyledText line height. --- .../CodeMiningLineHeaderAnnotation.java | 22 +++-- .../text/codemining/AbstractCodeMining.java | 2 +- .../text/codemining/LineHeaderCodeMining.java | 6 +- .../CodeMiningLineHeaderAnnotationTest.java | 94 +++++++++++++++++++ 4 files changed, 114 insertions(+), 10 deletions(-) diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java index 657e34cfd7f..45f814abf5c 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/internal/text/codemining/CodeMiningLineHeaderAnnotation.java @@ -133,20 +133,28 @@ static int getMultilineHeight(GC gc, List minings, StyledText style } private static int calculateLineHeight(List minings, GC gc, StyledText styledText, boolean ignoreFirstLine, int sumLineHeight, int miningIndex, String[] splitted) { + int styledTextLineHeight= 0; + if (styledText != null) { + styledTextLineHeight= styledText.getLineHeight(); + } for (int j= 0; j < splitted.length; j++) { String line= splitted[j]; if (j == 0 && ignoreFirstLine) { continue; } - if (j == splitted.length - 1 && miningIndex + 1 < minings.size()) { // last line, take first line from next mining - String nextLabel= minings.get(miningIndex + 1).getLabel(); - if (nextLabel != null) { - String firstFromNext= nextLabel.split("\\r?\\n|\\r")[0]; //$NON-NLS-1$ - line+= firstFromNext; + if (styledText != null) { + sumLineHeight+= styledTextLineHeight + styledText.getLineSpacing(); + } else { + if (j == splitted.length - 1 && miningIndex + 1 < minings.size()) { // last line, take first line from next mining + String nextLabel= minings.get(miningIndex + 1).getLabel(); + if (nextLabel != null) { + String firstFromNext= nextLabel.split("\\r?\\n|\\r")[0]; //$NON-NLS-1$ + line+= firstFromNext; + } } + Point ext= gc.textExtent(line); + sumLineHeight+= ext.y; } - Point ext= gc.textExtent(line); - sumLineHeight+= ext.y + (styledText != null ? styledText.getLineSpacing() : 0); } return sumLineHeight; } diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java index 4894583d6a1..3547e2daa6b 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/AbstractCodeMining.java @@ -145,7 +145,7 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { String title= getLabel() != null ? getLabel() : "no command"; //$NON-NLS-1$ gc.drawString(title, x, y, true); Point result= gc.stringExtent(title); - result.y+= textWidget.getLineSpacing(); + result.y= textWidget.getLineHeight() + textWidget.getLineSpacing(); return result; } diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java index 46721f2d736..8c310ee2d2e 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/codemining/LineHeaderCodeMining.java @@ -84,6 +84,8 @@ public Point draw(GC gc, StyledText textWidget, Color color, int x, int y) { } static Point draw(String label, GC gc, StyledText textWidget, int x, int y, Callable superDrawCallable) { + int lineHeight= textWidget.getLineHeight(); + int lineSpacing= textWidget.getLineSpacing(); String title= label != null ? label : "no command"; //$NON-NLS-1$ String[] lines= title.split("\\r?\\n|\\r"); //$NON-NLS-1$ if (lines.length > 1) { @@ -92,8 +94,8 @@ static Point draw(String label, GC gc, StyledText textWidget, int x, int y, Call gc.drawString(line, x, y, true); Point ext= gc.stringExtent(line); result.x= Math.max(result.x, ext.x); - result.y+= ext.y + textWidget.getLineSpacing(); - y+= ext.y + textWidget.getLineSpacing(); + result.y+= lineHeight + lineSpacing; + y+= lineHeight + lineSpacing; } return result; } else { diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java index 1fc1c03b33a..859e3294178 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningLineHeaderAnnotationTest.java @@ -14,6 +14,7 @@ import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -22,6 +23,8 @@ import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyledText; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.layout.FillLayout; @@ -123,4 +126,95 @@ public String getLabel() { // https: //github.com/eclipse-platform/eclipse.platform.ui/issues/2786 assertNotEquals(0, cut.getHeight()); // getHeight should not return 0, otherwise editor content starts jumping around } + + /** + * Verifies that a multi-line line-header code mining contributes a height which is an exact + * multiple of the StyledText's regular line height (plus line spacing). Otherwise the line + * drawn below the code mining would no longer be vertically aligned with the regular text + * lines. This was visible on Windows with Consolas at odd point sizes (e.g. 9, 11, 13), where + * {@code gc.stringExtent(line).y} returned a different value than + * {@link StyledText#getLineHeight()}, so the lines below a multiline code mining were shifted + * by a few pixels relative to the regular grid. + */ + @Test + public void testTwoLineCodeMiningHeightMatchesTextWidgetLineHeight() throws Exception { + var doc= fViewer.getDocument(); + doc.set("line0\nline1\nline2"); + var textWidget= fViewer.getTextWidget(); + // A line spacing != 0 makes a regression in either factor immediately visible. + textWidget.setLineSpacing(3); + // Try to use Consolas at an odd size on Windows since this is the configuration in which + // the original bug was reported. On other platforms / when Consolas is not available the + // test still runs with the default font - the assertions below must hold for any font. + Font consolasFont= tryCreateConsolasFont(textWidget, 11); + try { + if (consolasFont != null) { + textWidget.setFont(consolasFont); + } + String codeMiningLabel= "code mining line1\ncode mining line2"; + var mining= new LineHeaderCodeMining(0, doc, null, null) { + @Override + public String getLabel() { + return codeMiningLabel; + } + }; + int normalLineHeight= textWidget.getLineHeight(); + int lineSpacing= textWidget.getLineSpacing(); + int numMiningLines= codeMiningLabel.split("\n").length; + int expectedMiningHeight= numMiningLines * (normalLineHeight + lineSpacing); + + // 1) The size returned by LineHeaderCodeMining.draw() must be a multiple of the + // StyledText's regular line height + line spacing - it must not depend on + // gc.stringExtent(...).y, which on Consolas/odd sizes differs from getLineHeight(). + var gc= new GC(textWidget); + try { + Point result= mining.draw(gc, textWidget, null, 0, 0); + assertEquals(expectedMiningHeight, result.y, + "two-line code mining height must be 2 * (textWidget.getLineHeight() + lineSpacing)"); + } finally { + gc.dispose(); + } + + // 2) CodeMiningLineHeaderAnnotation.getHeight(GC) drives the line vertical indent of the + // line below the mining (see InlinedAnnotationDrawingStrategy). Its result must equal + // expectedMiningHeight - meaning the first regular text line below the mining ends up + // at exactly that pixel offset above its un-indented position, on the same vertical + // grid as all other text lines. + var annotation= new CodeMiningLineHeaderAnnotation(new Position(0, 0), fViewer); + var support= new InlinedAnnotationSupport(); + support.install(fViewer, new AnnotationPainter(fViewer, null)); + var setSupport= AbstractInlinedAnnotation.class.getDeclaredMethod("setSupport", InlinedAnnotationSupport.class); + setSupport.setAccessible(true); + setSupport.invoke(annotation, support); + annotation.update(List.of(mining), null); + + GC gc2= new GC(textWidget); + try { + int multilineHeight= annotation.getHeight(gc2); + assertEquals(expectedMiningHeight, multilineHeight, + "multiline mining height must be a whole-line multiple of the StyledText line height"); + // The pixel position of the line directly below the code mining is + // (multilineHeight) above its un-indented y position, and that offset must be a + // whole number of regular text line heights so the lines below the mining stay + // aligned with the regular grid. + assertEquals(0, multilineHeight % (normalLineHeight + lineSpacing), + "line below the code mining must land on a regular text-line boundary"); + } finally { + gc2.dispose(); + } + } finally { + if (consolasFont != null) { + textWidget.setFont(null); + consolasFont.dispose(); + } + } + } + + private static Font tryCreateConsolasFont(StyledText textWidget, int height) { + FontData[] available= textWidget.getDisplay().getFontList("Consolas", true); //$NON-NLS-1$ + if (available == null || available.length == 0) { + return null; + } + return new Font(textWidget.getDisplay(), "Consolas", height, SWT.NORMAL); //$NON-NLS-1$ + } } From ff1f1b794dcc7403532f36189964e33762b1dde1 Mon Sep 17 00:00:00 2001 From: Eclipse Platform Bot Date: Mon, 1 Jun 2026 07:26:58 +0000 Subject: [PATCH 07/19] Version bump(s) for 4.41 stream --- bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF b/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF index 653c7b05d9d..decf753be7c 100644 --- a/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF @@ -2,7 +2,7 @@ Manifest-Version: 1.0 Bundle-ManifestVersion: 2 Bundle-Name: %pluginName Bundle-SymbolicName: org.eclipse.jface.text -Bundle-Version: 3.31.0.qualifier +Bundle-Version: 3.31.100.qualifier Bundle-Vendor: %providerName Bundle-Localization: plugin Export-Package: From a6ae91ce2cc693e1c3e905737ca5fbd1c7abaacc Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 7 May 2026 07:52:46 +0200 Subject: [PATCH 08/19] Merge AbstractCSSSWTEngineImpl into CSSSWTEngineImpl Same housekeeping pattern as the previous core-engine merge: AbstractCSSSWTEngineImpl was an abstract layer with two abstract hooks (initializeCSSPropertyHandlers, initializeCSSElementProvider) whose only concrete subclass was CSSSWTEngineImpl. Inline the abstract layer into the concrete leaf, drop the two now-redundant template methods, and register the SWT element provider and the registry-driven property handler provider directly in the constructor. Bundle internal (x-friends only); no API surface changed. All 194 css.swt tests still pass. Contributes to #3980 --- .../swt/engine/AbstractCSSSWTEngineImpl.java | 141 ------------------ .../ui/css/swt/engine/CSSSWTEngineImpl.java | 107 ++++++++++--- 2 files changed, 90 insertions(+), 158 deletions(-) delete mode 100644 bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java deleted file mode 100644 index cb81d021c2d..00000000000 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/AbstractCSSSWTEngineImpl.java +++ /dev/null @@ -1,141 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2014 Angelo Zerr and others. - * - * 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 - * - * Contributors: - * Angelo Zerr - initial API and implementation - * IBM Corporation - *******************************************************************************/ -package org.eclipse.e4.ui.css.swt.engine; - -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.eclipse.e4.ui.css.core.engine.CSSElementContext; -import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; -import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; -import org.eclipse.e4.ui.css.swt.dom.WidgetElement; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTColorConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTCursorConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontDataConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTGradientConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTImageConverterImpl; -import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTRGBConverterImpl; -import org.eclipse.e4.ui.css.swt.resources.SWTResourceRegistryKeyFactory; -import org.eclipse.e4.ui.css.swt.resources.SWTResourcesRegistry; -import org.eclipse.swt.widgets.Display; -import org.eclipse.swt.widgets.Widget; -import org.w3c.dom.Element; - -/** - * CSS SWT Engine implementation which configure CSSEngineImpl to apply styles - * to SWT widgets. - */ -public abstract class AbstractCSSSWTEngineImpl extends CSSEngineImpl { - - protected Display display; - - public AbstractCSSSWTEngineImpl(Display display) { - this(display, false); - } - - public AbstractCSSSWTEngineImpl(Display display, boolean lazyApplyingStyles) { - this.display = display; - - /** Initialize SWT CSSValue converter * */ - - // Register SWT RGB CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTRGBConverterImpl.INSTANCE); - // Register SWT Color CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTColorConverterImpl.INSTANCE); - // Register SWT Gradient CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTGradientConverterImpl.INSTANCE); - // Register SWT Cursor CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTCursorConverterImpl.INSTANCE); - // Register SWT Font CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTFontConverterImpl.INSTANCE); - // Register SWT FontData CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTFontDataConverterImpl.INSTANCE); - // Register SWT Image CSSValue Converter - super.registerCSSValueConverter(CSSValueSWTImageConverterImpl.INSTANCE); - - if (lazyApplyingStyles) { - new CSSSWTApplyStylesListener(display, this); - } - - initializeCSSElementProvider(); - initializeCSSPropertyHandlers(); - - setResourceRegistryKeyFactory(new SWTResourceRegistryKeyFactory()); - } - - protected abstract void initializeCSSPropertyHandlers(); - - protected abstract void initializeCSSElementProvider(); - - @Override - public IResourcesRegistry getResourcesRegistry() { - IResourcesRegistry resourcesRegistry = super.getResourcesRegistry(); - if (resourcesRegistry == null) { - super.setResourcesRegistry(new SWTResourcesRegistry(display)); - } - return super.getResourcesRegistry(); - } - - @Override - public Element getElement(Object element) { - if (element instanceof CSSStylableElement - && ((CSSStylableElement) element).getNativeWidget() instanceof Widget) { - return (CSSStylableElement) element; - } else if (element instanceof Widget) { - if (isStylable((Widget) element)) { - return super.getElement(element); - } - } else { - // FIXME: we need to pass through the ThemeElementDefinitions; - // perhaps they should be handled by a separate engine - return super.getElement(element); - } - return null; - } - - /** - * Return true if the given widget can be styled - * - * @param widget - * the widget - * @return true if the widget can be styled - */ - protected boolean isStylable(Widget widget) { - // allows widgets to be selectively excluded from styling - return !widget.isDisposed() - && !Boolean.TRUE.equals(widget.getData("org.eclipse.e4.ui.css.disabled")); //$NON-NLS-1$ - } - - @Override - public void reset() { - for (CSSElementContext elementContext : getElementsContext().values()) { - Element element = elementContext.getElement(); - if (element instanceof WidgetElement - && isApplicableToReset((WidgetElement) element)) { - ((WidgetElement) element).reset(); - } - } - - getResourcesRegistry().dispose(); - super.reset(); - } - - private boolean isApplicableToReset(WidgetElement element) { - if (element.getNativeWidget() instanceof Widget) { - return !((Widget) element.getNativeWidget()).isDisposed(); - } - return false; - } - -} diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java index c406a848ea6..8bfc22e01c4 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2019 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -16,34 +16,63 @@ import org.eclipse.core.runtime.ILog; import org.eclipse.core.runtime.RegistryFactory; +import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; +import org.eclipse.e4.ui.css.core.engine.CSSElementContext; +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; import org.eclipse.e4.ui.css.core.impl.engine.RegistryCSSElementProvider; import org.eclipse.e4.ui.css.core.impl.engine.RegistryCSSPropertyHandlerProvider; +import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; +import org.eclipse.e4.ui.css.swt.dom.WidgetElement; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTColorConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTCursorConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTFontDataConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTGradientConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTImageConverterImpl; +import org.eclipse.e4.ui.css.swt.properties.converters.CSSValueSWTRGBConverterImpl; +import org.eclipse.e4.ui.css.swt.resources.SWTResourceRegistryKeyFactory; +import org.eclipse.e4.ui.css.swt.resources.SWTResourcesRegistry; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Widget; +import org.w3c.dom.Element; /** - * CSS SWT Engine implementation which configure CSSEngineImpl to apply styles - * to SWT widgets with static handler strategy. + * CSS SWT Engine. Configures {@link CSSEngineImpl} with the SWT-specific + * value converters and the registry-driven element + property handler + * providers, and applies styles to SWT widgets. */ -public class CSSSWTEngineImpl extends AbstractCSSSWTEngineImpl { +public class CSSSWTEngineImpl extends CSSEngineImpl { - private DisposeListener disposeListener; + protected Display display; + + private final DisposeListener disposeListener = e -> handleWidgetDisposed(e.widget); public CSSSWTEngineImpl(Display display) { - super(display); - init(); + this(display, false); } public CSSSWTEngineImpl(Display display, boolean lazyApplyingStyles) { - super(display, lazyApplyingStyles); - init(); - } + this.display = display; - private void init() { - disposeListener = e -> handleWidgetDisposed(e.widget); + registerCSSValueConverter(CSSValueSWTRGBConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTColorConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTGradientConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTCursorConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTFontConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTFontDataConverterImpl.INSTANCE); + registerCSSValueConverter(CSSValueSWTImageConverterImpl.INSTANCE); + + if (lazyApplyingStyles) { + new CSSSWTApplyStylesListener(display, this); + } + + setElementProvider(new RegistryCSSElementProvider(RegistryFactory.getRegistry())); + propertyHandlerProviders.add(new RegistryCSSPropertyHandlerProvider(RegistryFactory.getRegistry())); + + setResourceRegistryKeyFactory(new SWTResourceRegistryKeyFactory()); } @Override @@ -54,13 +83,58 @@ protected void hookNativeWidget(Object widget) { } @Override - protected void initializeCSSPropertyHandlers() { - propertyHandlerProviders.add(new RegistryCSSPropertyHandlerProvider(RegistryFactory.getRegistry())); + public IResourcesRegistry getResourcesRegistry() { + IResourcesRegistry resourcesRegistry = super.getResourcesRegistry(); + if (resourcesRegistry == null) { + super.setResourcesRegistry(new SWTResourcesRegistry(display)); + } + return super.getResourcesRegistry(); } @Override - protected void initializeCSSElementProvider() { - setElementProvider(new RegistryCSSElementProvider(RegistryFactory.getRegistry())); + public Element getElement(Object element) { + if (element instanceof CSSStylableElement + && ((CSSStylableElement) element).getNativeWidget() instanceof Widget) { + return (CSSStylableElement) element; + } else if (element instanceof Widget) { + if (isStylable((Widget) element)) { + return super.getElement(element); + } + } else { + // FIXME: we need to pass through the ThemeElementDefinitions; + // perhaps they should be handled by a separate engine + return super.getElement(element); + } + return null; + } + + /** + * Return true if the given widget can be styled. + */ + protected boolean isStylable(Widget widget) { + return !widget.isDisposed() + && !Boolean.TRUE.equals(widget.getData("org.eclipse.e4.ui.css.disabled")); //$NON-NLS-1$ + } + + @Override + public void reset() { + for (CSSElementContext elementContext : getElementsContext().values()) { + Element element = elementContext.getElement(); + if (element instanceof WidgetElement + && isApplicableToReset((WidgetElement) element)) { + ((WidgetElement) element).reset(); + } + } + + getResourcesRegistry().dispose(); + super.reset(); + } + + private boolean isApplicableToReset(WidgetElement element) { + if (element.getNativeWidget() instanceof Widget) { + return !((Widget) element.getNativeWidget()).isDisposed(); + } + return false; } @Override @@ -78,5 +152,4 @@ public void reapply() { } } } - } From 59cc8cc974297f6d80ebf492bd3161b771d1a84e Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Thu, 14 May 2026 09:16:06 +0200 Subject: [PATCH 09/19] Add tests for find/replace UI history store and overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The history feature of the find/replace UI lacks test coverage in two distinct areas: HistoryStore (unit tests): HistoryStoreTest covers the storage class in isolation — ordering (newest-first), deduplication, size-limit enforcement, remove semantics, persistence across instances sharing the same IDialogSettings, and independence of separate sections. Unit tests are necessary here because the class carries non-trivial logic (e.g. the in-place deduplication loop in write()) that should be verified without spinning up the Eclipse workbench, and because failures in this layer would otherwise only surface as hard-to-diagnose UI test failures. Find/replace UI (integration tests): five new tests in FindReplaceUITest verify that history is correctly populated after search, replace-all, and single replace, and that entries are ordered newest-first with duplicates suppressed. Running them in the common superclass ensures both the overlay and the dialog are covered. IFindReplaceUIAccess gains selectFindHistoryEntry(int) to abstract over the two different navigation mechanisms (ARROW_DOWN in the overlay text field, combo selection in the dialog). Two additional tests remain in FindReplaceOverlayTest to cover the overlay-specific forward and backward search buttons, which have no dialog counterpart. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/2100 --- .../META-INF/MANIFEST.MF | 7 +- .../findandreplace/FindReplaceUITest.java | 74 +++++ .../findandreplace/HistoryStoreTest.java | 255 ++++++++++++++++++ .../findandreplace/IFindReplaceUIAccess.java | 2 + .../overlay/FindReplaceOverlayTest.java | 31 +++ .../findandreplace/overlay/OverlayAccess.java | 11 + .../texteditor/tests/DialogAccess.java | 5 + .../tests/WorkbenchTextEditorTestSuite.java | 2 + 8 files changed, 384 insertions(+), 3 deletions(-) create mode 100644 tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF index c39e7b8fbdd..8fc2605ffed 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/META-INF/MANIFEST.MF @@ -22,9 +22,10 @@ Require-Bundle: Bundle-RequiredExecutionEnvironment: JavaSE-21 Eclipse-BundleShape: dir Automatic-Module-Name: org.eclipse.ui.workbench.texteditor.tests -Import-Package: org.mockito, - org.mockito.stubbing;version="5.5.0", - org.junit.jupiter.api;version="[5.14.0,6.0.0)", +Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.function;version="[5.14.0,6.0.0)", org.junit.platform.suite.api;version="[1.14.0,2.0.0)", + org.mockito, + org.mockito.invocation;version="[5.23.0,6.0.0)", + org.mockito.stubbing;version="5.5.0", org.opentest4j;version="[1.3.0,2.0.0)" diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java index c253586ccc3..efca3df0db4 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/FindReplaceUITest.java @@ -376,6 +376,80 @@ public void testReplaceIfSelectedOnStart() { assertThat(fTextViewer.getDocument().get(), is("abaaefg")); } + @Test + public void testSearchTermStoredInHistoryAfterSearch() { + // Performing a search must persist the search term so that the user can + // navigate back to it via history in a subsequent session. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchHistoryContainsAllRecentTermsNewestFirst() { + // Multiple searches must all appear in history ordered newest-first, so that + // index 0 always yields the most recently used term. + initializeTextViewerWithFindReplaceUI("foo bar baz foo bar baz"); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + dialog.setFindText("bar"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("bar", dialog.getFindText()); + + dialog.selectFindHistoryEntry(1); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchHistoryDeduplicatesRepeatedSearchTerms() { + // Searching for the same term twice must not create a duplicate entry in + // history. If it did, index 1 would show "foo" again instead of "bar". + initializeTextViewerWithFindReplaceUI("foo bar foo bar"); + dialog.setFindText("bar"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + // Search "foo" a second time — must not insert a second "foo" entry. + dialog.setFindText("foo"); + dialog.simulateKeyboardInteractionInFindInputField(SWT.CR, false); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + // A duplicate "foo" would appear here instead of "bar". + dialog.selectFindHistoryEntry(1); + assertEquals("bar", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterReplaceAll() { + // A replace-all operation must persist the search term to history so that + // it is available for future searches. + initializeTextViewerWithFindReplaceUI("foo foo foo"); + dialog.setFindText("foo"); + dialog.setReplaceText("bar"); + dialog.performReplaceAll(); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterSingleReplace() { + // A single replace operation must also persist the search term to history. + initializeTextViewerWithFindReplaceUI("foo bar"); + dialog.setFindText("foo"); + dialog.setReplaceText("baz"); + dialog.performReplace(); + + dialog.selectFindHistoryEntry(0); + assertEquals("foo", dialog.getFindText()); + } + protected AccessType getDialog() { return dialog; } diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java new file mode 100644 index 00000000000..ddc5cdd285d --- /dev/null +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/HistoryStoreTest.java @@ -0,0 +1,255 @@ +/******************************************************************************* + * Copyright (c) 2025 Vector Informatik GmbH and others. + * + * 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 + * + * Contributors: + * Vector Informatik GmbH - initial API and implementation + *******************************************************************************/ +package org.eclipse.ui.internal.findandreplace; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import org.eclipse.jface.dialogs.IDialogSettings; + +public class HistoryStoreTest { + + /** + * Returns a minimal {@link IDialogSettings} stub that stores and retrieves + * {@code String[]} values in memory, keyed by section name. All other + * {@code IDialogSettings} methods are left as Mockito no-ops / default returns. + */ + private IDialogSettings createInMemoryDialogSettings() { + Map store = new HashMap<>(); + IDialogSettings settings = mock(IDialogSettings.class); + when(settings.getArray(anyString())).thenAnswer(inv -> store.get(inv.getArgument(0))); + doAnswer(inv -> { + store.put(inv.getArgument(0), inv.getArgument(1)); + return null; + }).when(settings).put(anyString(), any(String[].class)); + return settings; + } + + @Test + public void testConstructorThrowsOnNullSectionName() { + assertThrows(IllegalStateException.class, + () -> new HistoryStore(createInMemoryDialogSettings(), null, 5)); + } + + @Test + public void testNewStoreIsEmpty() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + assertTrue(store.isEmpty()); + assertEquals(0, store.size()); + } + + @Test + public void testAddSingleItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add("item"); + + assertFalse(store.isEmpty()); + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testAddNullIsIgnored() { + // Null items must not be stored; history stays empty. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add(null); + + assertTrue(store.isEmpty()); + } + + @Test + public void testAddEmptyStringIsIgnored() { + // Empty strings must not be stored; an empty search term is not useful history. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add(""); + + assertTrue(store.isEmpty()); + } + + @Test + public void testMostRecentlyAddedItemIsFirst() { + // The most recently added item should appear at index 0 so that history + // navigation reaches recent searches first. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + + store.add("first"); + store.add("second"); + store.add("third"); + + assertEquals("third", store.get(0)); + assertEquals("second", store.get(1)); + assertEquals("first", store.get(2)); + assertEquals(3, store.size()); + } + + @Test + public void testAddingExistingItemMovesItToFront() { + // Re-adding a term already in history should move it to index 0 without + // creating a duplicate. This mirrors how every search toolbar works: the most + // recently used term comes first. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.add("first"); + + assertEquals(2, store.size()); + assertEquals("first", store.get(0)); + assertEquals("second", store.get(1)); + } + + @Test + public void testAddingItemAlreadyAtFrontKeepsItThere() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + store.add("item"); + + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testHistorySizeLimitDropsOldestEntries() { + // Once the capacity is reached, the oldest (highest-index) entry must be + // dropped to make room for the new one. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 3); + store.add("first"); + store.add("second"); + store.add("third"); + + store.add("fourth"); + + assertEquals(3, store.size()); + assertEquals("fourth", store.get(0)); + assertEquals("third", store.get(1)); + assertEquals("second", store.get(2)); + } + + @Test + public void testRemoveExistingItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.remove("first"); + + assertEquals(1, store.size()); + assertEquals("second", store.get(0)); + } + + @Test + public void testRemoveItemAtFront() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + store.remove("second"); + + assertEquals(1, store.size()); + assertEquals("first", store.get(0)); + } + + @Test + public void testRemoveNonExistentItemIsNoOp() { + // Removing a term that was never stored must not alter the history. + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + store.remove("nonexistent"); + + assertEquals(1, store.size()); + assertEquals("item", store.get(0)); + } + + @Test + public void testIndexOfReturnsCorrectPositions() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + store.add("third"); + + assertEquals(0, store.indexOf("third")); + assertEquals(1, store.indexOf("second")); + assertEquals(2, store.indexOf("first")); + } + + @Test + public void testIndexOfReturnsMinusOneForAbsentItem() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("item"); + + assertEquals(-1, store.indexOf("nonexistent")); + } + + @Test + public void testGetIterableReturnsItemsNewestFirst() { + HistoryStore store = new HistoryStore(createInMemoryDialogSettings(), "section", 5); + store.add("first"); + store.add("second"); + + List items = new ArrayList<>(); + for (String item : store.get()) { + items.add(item); + } + + assertEquals(List.of("second", "first"), items); + } + + @Test + public void testHistoryPersistedAcrossInstances() { + // Two HistoryStore instances pointing to the same IDialogSettings section must + // share the same data, modelling persistence across workbench sessions. + IDialogSettings sharedSettings = createInMemoryDialogSettings(); + HistoryStore store1 = new HistoryStore(sharedSettings, "section", 5); + store1.add("first"); + store1.add("second"); + + HistoryStore store2 = new HistoryStore(sharedSettings, "section", 5); + + assertEquals(2, store2.size()); + assertEquals("second", store2.get(0)); + assertEquals("first", store2.get(1)); + } + + @Test + public void testDistinctSectionsAreIndependent() { + // Two stores sharing the same IDialogSettings but using different section names + // must not interfere with each other (models separate find/replace histories). + IDialogSettings sharedSettings = createInMemoryDialogSettings(); + HistoryStore findHistory = new HistoryStore(sharedSettings, "findhistory", 5); + HistoryStore replaceHistory = new HistoryStore(sharedSettings, "replacehistory", 5); + findHistory.add("findterm"); + + assertTrue(replaceHistory.isEmpty()); + } + +} diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java index 57a4edd6f04..55d60f7f685 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/IFindReplaceUIAccess.java @@ -49,6 +49,8 @@ public interface IFindReplaceUIAccess { void performReplaceAndFind(); + void selectFindHistoryEntry(int index); + void assertInitialConfiguration(); void assertUnselected(SearchOptions option); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java index ee073791589..e74eb92d3b4 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/FindReplaceOverlayTest.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; +import org.eclipse.swt.SWT; import org.eclipse.swt.graphics.Point; import org.eclipse.core.runtime.preferences.IEclipsePreferences; @@ -184,4 +185,34 @@ public void testDisableOverlayViaPreference() { } } + @Test + public void testSearchTermStoredInHistoryAfterSearchForward() { + // After a forward search, the term must be retrievable from history so that + // the user can navigate back to it in a subsequent session. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + OverlayAccess dialog= getDialog(); + dialog.setFindText("foo"); + dialog.pressSearch(true); + + // Down-arrow navigates to the most recently stored entry (index 0). + dialog.setFindText(""); + dialog.simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + + assertEquals("foo", dialog.getFindText()); + } + + @Test + public void testSearchTermStoredInHistoryAfterSearchBackward() { + // Backward search must persist the term to history just like forward search. + initializeTextViewerWithFindReplaceUI("foo bar foo"); + OverlayAccess dialog= getDialog(); + dialog.setFindText("foo"); + dialog.pressSearch(false); + + dialog.setFindText(""); + dialog.simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + + assertEquals("foo", dialog.getFindText()); + } + } diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java index ae07a9643ec..16de0671b77 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/internal/findandreplace/overlay/OverlayAccess.java @@ -216,6 +216,17 @@ private Predicate isOptionSelected() { }; } + @Override + public void selectFindHistoryEntry(int index) { + // Clear the field directly (no SWT.Modify, so no incremental search fires), + // then step down once per index position. From an empty field ARROW_DOWN + // always lands on index 0 (newest); each additional press moves one step older. + find.setText(""); + for (int i = 0; i <= index; i++) { + simulateKeyboardInteractionInFindInputField(SWT.ARROW_DOWN, false); + } + } + public void pressSearch(boolean forward) { if (forward) { searchForward.notifyListeners(SWT.Selection, null); diff --git a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java index 29247c3c0f1..9d5a10dddb0 100644 --- a/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java +++ b/tests/org.eclipse.ui.workbench.texteditor.tests/src/org/eclipse/ui/workbench/texteditor/tests/DialogAccess.java @@ -230,6 +230,11 @@ public void performReplace() { } } + @Override + public void selectFindHistoryEntry(int index) { + findCombo.select(index); + } + public void performFindNext() { if (findNextButton.getEnabled()) { findNextButton.notifyListeners(SWT.Selection, null); 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 30ba98d200c..d2ad80884ed 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 @@ -17,6 +17,7 @@ import org.junit.platform.suite.api.SelectClasses; import org.eclipse.ui.internal.findandreplace.FindReplaceLogicTest; +import org.eclipse.ui.internal.findandreplace.HistoryStoreTest; import org.eclipse.ui.internal.findandreplace.overlay.FindReplaceOverlayTest; import org.eclipse.ui.workbench.texteditor.tests.minimap.MinimapPageTest; @@ -48,6 +49,7 @@ FindReplaceDialogTest.class, FindReplaceOverlayTest.class, FindReplaceLogicTest.class, + HistoryStoreTest.class, }) public class WorkbenchTextEditorTestSuite { // see @SelectClasses From bd473bb7e233959372c5bcb7d89055ecbacc20f0 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 7 May 2026 07:56:55 +0200 Subject: [PATCH 10/19] Fold ICSSPropertyHandler2 into ICSSPropertyHandler ICSSPropertyHandler2 added a single capability on top of ICSSPropertyHandler: a callback the engine fires after every property in a style declaration has been applied. Java 21 default methods make the second interface redundant: move the two onAllCSSPropertiesApplyed defaults onto ICSSPropertyHandler so every handler picks up a no-op default and only handlers that need the final-step hook (border, font) override it. ICSSPropertyHandler2Delegate had no implementations in the platform tree at all, so its branch in CSSEngineImpl.applyStyleDeclaration was dead. Drop the interface and the dead branch in the same pass. CSSEngineImpl.applyStyleDeclaration now collects every applied ICSSPropertyHandler (de-duped, order-preserving) and fires the callback on each. The previous singleton-list cache stays, just keyed on ICSSPropertyHandler instead of ICSSPropertyHandler2. CSSPropertyBorderSWTHandler and CSSPropertyFontSWTHandler drop their explicit "implements ICSSPropertyHandler2" since they already extend ICSSPropertyHandler-compatible bases. All bundles internal (x-friends only); no API surface changed. Contributes to #3980 --- .../dom/properties/ICSSPropertyHandler.java | 19 +++++++++ .../dom/properties/ICSSPropertyHandler2.java | 39 ------------------- .../ICSSPropertyHandler2Delegate.java | 30 -------------- .../css/core/impl/engine/CSSEngineImpl.java | 30 ++++++-------- .../css2/CSSPropertyBorderSWTHandler.java | 6 +-- .../css2/CSSPropertyFontSWTHandler.java | 6 +-- 6 files changed, 34 insertions(+), 96 deletions(-) delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java index c7cef57e669..c7279da95be 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler.java @@ -63,4 +63,23 @@ public default String retrieveCSSProperty(Object element, String property, Strin return null; } + /** + * Callback method called once after all CSS properties of a single + * declaration have been applied. Handlers that need to perform a final + * step (re-layout, redraw, batched commit, ...) override this method; + * the default is a no-op. + */ + default void onAllCSSPropertiesApplied(Object element, CSSEngine engine) throws Exception { + // do nothing + } + + /** + * Variant of {@link #onAllCSSPropertiesApplied(Object, CSSEngine)} that + * also receives the pseudo class. Defaults to delegating to the + * pseudo-less form. + */ + default void onAllCSSPropertiesApplied(Object element, CSSEngine engine, String pseudo) throws Exception { + onAllCSSPropertiesApplied(element, engine); + } + } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java deleted file mode 100644 index 7461186491f..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2.java +++ /dev/null @@ -1,39 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2015 Angelo Zerr and others. - * - * 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 - * - * Contributors: - * Angelo Zerr - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.dom.properties; - -import org.eclipse.e4.ui.css.core.engine.CSSEngine; - -/** - * CSS Property Handler to intercept when all CSS Properties are applied. - * - * @version 1.0.0 - * @author Angelo ZERR - */ -public interface ICSSPropertyHandler2 { - - /** - * Callback method called when all CSS properties are applied. - */ - default void onAllCSSPropertiesApplyed(Object element, CSSEngine engine) throws Exception { - // do nothing - } - - /** - * Callback method called when all CSS properties are applied. - */ - default void onAllCSSPropertiesApplyed(Object element, CSSEngine engine, String pseudo) throws Exception { - onAllCSSPropertiesApplyed(element, engine); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java deleted file mode 100644 index db26da9c36e..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/properties/ICSSPropertyHandler2Delegate.java +++ /dev/null @@ -1,30 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2008, 2013 Angelo Zerr and others. - * - * 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 - * - * Contributors: - * Angelo Zerr - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.dom.properties; - -/** - * - * {@link ICSSPropertyHandler2} delegate. - * - * @version 1.0.0 - * @author Angelo ZERR - */ -public interface ICSSPropertyHandler2Delegate { - - /** - * Return {@link ICSSPropertyHandler2} to call when all CSS Properties are - * applied . - */ - public ICSSPropertyHandler2 getCSSPropertyHandler2(); -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java index bfa459895e9..2fb2d1829de 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java @@ -49,8 +49,6 @@ import org.eclipse.e4.ui.css.core.dom.parsers.ICSSParserFactory; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyCompositeHandler; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2Delegate; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandlerProvider; import org.eclipse.e4.ui.css.core.dom.properties.converters.CSSValueBooleanConverterImpl; import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; @@ -155,7 +153,7 @@ public abstract class CSSEngineImpl implements CSSEngine { */ protected List propertyHandlerProviders = new ArrayList<>(); // for performance hold a map of handlers to singleton list - private final Map> propertyHandler2InstanceMap = new HashMap<>(); + private final Map> propertyHandlerInstanceMap = new HashMap<>(); private Map currentCSSPropertiesApplied; @@ -559,31 +557,25 @@ public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, Str if (avoidanceCacheInstalled) { currentCSSPropertiesApplied = new HashMap<>(); } - List handlers2 = Collections.emptyList(); + List appliedHandlers = Collections.emptyList(); for (int i = 0; i < style.getLength(); i++) { String property = style.item(i); CSSValue value = style.getPropertyCSSValue(property); try { ICSSPropertyHandler handler = this.applyCSSProperty(element, property, value, pseudo); - ICSSPropertyHandler2 propertyHandler2 = null; - if (handler instanceof ICSSPropertyHandler2) { - propertyHandler2 = (ICSSPropertyHandler2) handler; - } else if (handler instanceof ICSSPropertyHandler2Delegate) { - propertyHandler2 = ((ICSSPropertyHandler2Delegate) handler).getCSSPropertyHandler2(); - } - if (propertyHandler2 != null) { - switch (handlers2.size()) { + if (handler != null) { + switch (appliedHandlers.size()) { case 0: - handlers2 = propertyHandler2InstanceMap.computeIfAbsent(propertyHandler2, + appliedHandlers = propertyHandlerInstanceMap.computeIfAbsent(handler, Collections::singletonList); break; case 1: - handlers2 = new ArrayList<>(handlers2); - handlers2.add(propertyHandler2); + appliedHandlers = new ArrayList<>(appliedHandlers); + appliedHandlers.add(handler); break; default: - if (!handlers2.contains(propertyHandler2)) { - handlers2.add(propertyHandler2); + if (!appliedHandlers.contains(handler)) { + appliedHandlers.add(handler); } } } @@ -593,9 +585,9 @@ public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, Str } } } - for (ICSSPropertyHandler2 handler2 : handlers2) { + for (ICSSPropertyHandler handler : appliedHandlers) { try { - handler2.onAllCSSPropertiesApplyed(element, this, pseudo); + handler.onAllCSSPropertiesApplied(element, this, pseudo); } catch (Exception e) { handleExceptions(e); } diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java index f8a46ac2858..e7a4c6d0802 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyBorderSWTHandler.java @@ -14,7 +14,6 @@ package org.eclipse.e4.ui.css.swt.properties.css2; import org.eclipse.e4.ui.css.core.dom.properties.CSSBorderProperties; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; import org.eclipse.e4.ui.css.core.dom.properties.css2.AbstractCSSPropertyBorderHandler; import org.eclipse.e4.ui.css.core.dom.properties.css2.ICSSPropertyBorderHandler; import org.eclipse.e4.ui.css.core.engine.CSSEngine; @@ -28,8 +27,7 @@ import org.w3c.dom.css.CSSPrimitiveValue; import org.w3c.dom.css.CSSValue; -public class CSSPropertyBorderSWTHandler extends -AbstractCSSPropertyBorderHandler implements ICSSPropertyHandler2 { +public class CSSPropertyBorderSWTHandler extends AbstractCSSPropertyBorderHandler { public static final ICSSPropertyBorderHandler INSTANCE = new CSSPropertyBorderSWTHandler(); @@ -70,7 +68,7 @@ public boolean applyCSSProperty(Object element, String property, } @Override - public void onAllCSSPropertiesApplyed(Object element, CSSEngine engine) + public void onAllCSSPropertiesApplied(Object element, CSSEngine engine) throws Exception { Control control = SWTElementHelpers.getControl(element); if (control != null) { diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java index 886c70f8051..a5a6afe595b 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/properties/css2/CSSPropertyFontSWTHandler.java @@ -16,7 +16,6 @@ *******************************************************************************/ package org.eclipse.e4.ui.css.swt.properties.css2; -import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler2; import org.eclipse.e4.ui.css.core.dom.properties.css2.AbstractCSSPropertyFontHandler; import org.eclipse.e4.ui.css.core.dom.properties.css2.CSS2FontProperties; import org.eclipse.e4.ui.css.core.dom.properties.css2.ICSSPropertyFontHandler; @@ -39,8 +38,7 @@ import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSValue; -public class CSSPropertyFontSWTHandler extends AbstractCSSPropertyFontHandler -implements ICSSPropertyHandler2 { +public class CSSPropertyFontSWTHandler extends AbstractCSSPropertyFontHandler { public static final ICSSPropertyFontHandler INSTANCE = new CSSPropertyFontSWTHandler(); @@ -232,7 +230,7 @@ public String retrieveCSSPropertyFontWeight(Object element, String pseudo, } @Override - public void onAllCSSPropertiesApplyed(Object element, CSSEngine engine) + public void onAllCSSPropertiesApplied(Object element, CSSEngine engine) throws Exception { final Widget widget = SWTElementHelpers.getWidget(element); if (widget == null || widget instanceof CTabItem) { From 52d8fa8579c4f717f1382cc2924b5724773f5339 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Fri, 8 May 2026 01:34:19 +0200 Subject: [PATCH 11/19] Use java.text.BreakIterator in DefaultTextDoubleClickStrategy Replaces com.ibm.icu.text.BreakIterator with java.text.BreakIterator in DefaultTextDoubleClickStrategy and drops the matching Import-Package: com.ibm.icu.text from the bundle manifest. The JDK BreakIterator exposes the same API (getWordInstance, preceding, following, isBoundary, setText, DONE) and the existing POSIX-locale workaround for '.' not being treated as a word boundary continues to work with java.text. Removes the last com.ibm.icu reference from org.eclipse.jface.text. --- .../META-INF/MANIFEST.MF | 1 - .../text/DefaultTextDoubleClickStrategy.java | 49 +++++++- .../DefaultTextDoubleClickStrategyTest.java | 107 ++++++++++++++++++ 3 files changed, 153 insertions(+), 4 deletions(-) diff --git a/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF b/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF index decf753be7c..ef7567b50e6 100644 --- a/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.jface.text/META-INF/MANIFEST.MF @@ -38,7 +38,6 @@ Require-Bundle: org.eclipse.text;bundle-version="[3.8.0,4.0.0)";visibility:=reexport, org.eclipse.swt;bundle-version="[3.133.0,4.0.0)", org.eclipse.jface;bundle-version="[3.39.0,4.0.0)" -Import-Package: com.ibm.icu.text Bundle-RequiredExecutionEnvironment: JavaSE-21 Automatic-Module-Name: org.eclipse.jface.text Bundle-Activator: org.eclipse.jface.text.Activator diff --git a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java index 6b1b370b8e4..c814e8f60be 100644 --- a/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java +++ b/bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/DefaultTextDoubleClickStrategy.java @@ -14,12 +14,10 @@ package org.eclipse.jface.text; +import java.text.BreakIterator; import java.text.CharacterIterator; import java.util.Locale; -import com.ibm.icu.text.BreakIterator; - - /** * Standard implementation of @@ -223,9 +221,54 @@ protected IRegion findExtendedDoubleClickSelection(IDocument document, int offse * @since 3.5 */ protected IRegion findWord(IDocument document, int offset) { + IRegion identifier= findIdentifierAt(document, offset); + if (identifier != null) { + return identifier; + } return findWord(document, offset, getWordBreakIterator()); } + /** + * If the offset lies on an ASCII identifier character ({@code [A-Za-z0-9_]}), or + * just after one, returns the maximal contiguous identifier run. Otherwise + * returns {@code null} so the caller falls back to the locale-aware + * {@link BreakIterator}. This handles identifier-style words containing runs + * of {@code '_'} (e.g. {@code foo__bar}, {@code __aaaa}) consistently across + * JDK versions, since {@link BreakIterator#getWordInstance()} places word + * boundaries between consecutive underscores while users expect such tokens + * to be selected as a single word. + */ + private static IRegion findIdentifierAt(IDocument document, int offset) { + try { + IRegion line= document.getLineInformationOfOffset(offset); + int lineStart= line.getOffset(); + int lineEnd= lineStart + line.getLength(); + int probe; + if (offset < lineEnd && isIdentifierPart(document.getChar(offset))) { + probe= offset; + } else if (offset > lineStart && isIdentifierPart(document.getChar(offset - 1))) { + probe= offset - 1; + } else { + return null; + } + int start= probe; + while (start > lineStart && isIdentifierPart(document.getChar(start - 1))) { + start--; + } + int end= probe + 1; + while (end < lineEnd && isIdentifierPart(document.getChar(end))) { + end++; + } + return new Region(start, end - start); + } catch (BadLocationException e) { + return null; + } + } + + private static boolean isIdentifierPart(char c) { + return c == '_' || (c < 128 && (c >= '0' && c <= '9' || c >= 'A' && c <= 'Z' || c >= 'a' && c <= 'z')); + } + /** * Returns the locale specific word break iterator. * diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java index 450d94d729b..f706ebb32a1 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/DefaultTextDoubleClickStrategyTest.java @@ -55,6 +55,113 @@ public void testClickAtLineEnd() throws Exception { assertEquals("you", document.get(selection.getOffset(), selection.getLength()), "Unexpected selection"); } + @Test + public void testClickJustPastIdentifierSelectsThatIdentifier() throws Exception { + String content= "foo bar baz"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click at offset 3: the space right after "foo". + IRegion selection= strategy.findWord(document, 3); + assertNotNull(selection); + assertEquals("foo", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testClickAtIdentifierStartSelectsWholeIdentifier() throws Exception { + String content= "foo __aaaa bar"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click at offset 4: the first '_' starting "__aaaa". + IRegion selection= strategy.findWord(document, 4); + assertNotNull(selection); + assertEquals("__aaaa", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testIdentifierAtLineStartAndEnd() throws Exception { + String content= "_foo___\nbar_baz"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // First line: every offset 0..7 should yield "_foo___". + for (int offset= 0; offset <= 7; offset++) { + IRegion selection= strategy.findWord(document, offset); + assertNotNull(selection, "no selection at offset " + offset); + assertEquals("_foo___", document.get(selection.getOffset(), selection.getLength()), + "unexpected selection at offset " + offset); + } + // Second line. + IRegion selection= strategy.findWord(document, 11); + assertNotNull(selection); + assertEquals("bar_baz", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testSingleLineDocument() throws Exception { + String content= "abc"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + IRegion selection= strategy.findWord(document, 0); + assertNotNull(selection); + assertEquals("abc", document.get(selection.getOffset(), selection.getLength())); + selection= strategy.findWord(document, document.getLength()); + assertNotNull(selection); + assertEquals("abc", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testIdentifierSurroundedByPunctuation() throws Exception { + String content= "(foo_bar);"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click in the middle of the identifier. + IRegion selection= strategy.findWord(document, 4); + assertNotNull(selection); + assertEquals("foo_bar", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testCjkWordSelection() throws Exception { + // Japanese text without spaces. The word break iterator segments it into a + // Hiragana run ("こんにちは") followed by a Kanji run + // ("世界"). This segmentation is locale-independent, so double-click + // selects the script run the click lands in rather than the whole line. + String content= "こんにちは世界"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click inside the Hiragana run. + IRegion selection= strategy.findWord(document, 2); + assertNotNull(selection); + assertEquals("こんにちは", document.get(selection.getOffset(), selection.getLength())); + // Click inside the Kanji run. + selection= strategy.findWord(document, 6); + assertNotNull(selection); + assertEquals("世界", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testCjkTokenBetweenSpaces() throws Exception { + String content= "foo 我是 bar"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + // Click inside the CJK token. + IRegion selection= strategy.findWord(document, 5); + assertNotNull(selection); + assertEquals("我是", document.get(selection.getOffset(), selection.getLength())); + } + + @Test + public void testThaiTokenBetweenSpaces() throws Exception { + // Dictionary-based segmentation of a contiguous Thai run only happens under a + // Thai locale, so this test delimits the token with spaces to stay + // locale-independent: double-click selects the whole Thai token. + String content= "foo ไทย bar"; + IDocument document= new Document(content); + TestSpecificDefaultTextDoubleClickStrategy strategy= new TestSpecificDefaultTextDoubleClickStrategy(); + IRegion selection= strategy.findWord(document, 5); + assertNotNull(selection); + assertEquals("ไทย", document.get(selection.getOffset(), selection.getLength())); + } + private static final class TestSpecificDefaultTextDoubleClickStrategy extends DefaultTextDoubleClickStrategy { @Override From 942953b5f66217404eff69df87149bfaed33c314 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 7 May 2026 07:58:27 +0200 Subject: [PATCH 12/19] Drop unused PropertyHelper and its self-test PropertyHelper provided a reflective bean-property reader using java.beans.Introspector. Nothing in the platform tree calls it; its only consumer was TestPropertyHelper, a unit test that exercised the helper itself. Delete the helper and the orphaned test in one pass. Contributes to #3980 --- .../e4/ui/css/swt/helpers/PropertyHelper.java | 80 ------------------ .../ui/tests/css/swt/TestPropertyHelper.java | 84 ------------------- 2 files changed, 164 deletions(-) delete mode 100644 bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java delete mode 100644 tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java deleted file mode 100644 index b018a6beb84..00000000000 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/helpers/PropertyHelper.java +++ /dev/null @@ -1,80 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2010, 2015 Tom Schindl and others. - * - * 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 - * - * Contributors: - * Tom Schindl - initial API and implementation - *******************************************************************************/ -package org.eclipse.e4.ui.css.swt.helpers; - -import java.util.HashMap; - -import java.lang.reflect.Method; - -import java.util.Map; - -import java.beans.IntrospectionException; -import java.beans.Introspector; -import java.beans.PropertyDescriptor; - -public class PropertyHelper { - private static final Map NOTNESTEDCACHE = new HashMap<>(); - - public static Object getProperty(Object bean, String attr) - throws Exception { - String key = bean.getClass().getName() + "#" + attr; - - if (attr.indexOf('.') == -1) { - Method readMethod = NOTNESTEDCACHE.get(key); - if (readMethod != null) { - return readMethod.invoke(bean); - } - } - - Method readMethod = null; - Object value = bean; - for (String part : attr.split("\\.")) { - PropertyDescriptor desc = getPropertyDescriptor(value.getClass(), - part); - if (desc != null) { - readMethod = desc.getReadMethod(); - } - - if (readMethod == null) { - throw new IllegalArgumentException("Attribute '" + part - + "' is not known in '" + value + "'"); - } else { - value = readMethod.invoke(value); - } - } - - if (attr.indexOf('.') == -1) { - NOTNESTEDCACHE.put(key,readMethod); - } - - return value; - } - - private static PropertyDescriptor getPropertyDescriptor(Class clazz, - String name) throws IntrospectionException { - PropertyDescriptor[] descs = getPropertyDescriptor(clazz); - for (PropertyDescriptor desc : descs) { - if (desc.getName().equals(name)) { - return desc; - } - } - return null; - } - - private static PropertyDescriptor[] getPropertyDescriptor(Class clazz) - throws IntrospectionException { - return Introspector.getBeanInfo(clazz).getPropertyDescriptors(); - } - -} \ No newline at end of file diff --git a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java b/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java deleted file mode 100644 index 2b40fc72ff3..00000000000 --- a/tests/org.eclipse.e4.ui.tests.css.swt/src/org/eclipse/e4/ui/tests/css/swt/TestPropertyHelper.java +++ /dev/null @@ -1,84 +0,0 @@ -/******************************************************************************* - * Copyright (c) 2013, 2014 IBM Corporation and others. - * - * 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 - * - * Contributors: - * IBM Corporation - initial API and implementation - * Thibault Le Ouay - Bug 443094 - *******************************************************************************/ -package org.eclipse.e4.ui.tests.css.swt; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.eclipse.e4.ui.css.swt.helpers.PropertyHelper; -import org.junit.jupiter.api.Test; - -public class TestPropertyHelper { - public static class Base { - private String a = "A"; - public String getA() { - return a; - } - public void setA(String a) { - this.a = a; - } - - public String getC() { - return "C"; - } - - public boolean isD() { - return true; - } - } - - public static class Impl extends Base { - private String b = "B"; - private Base nested = new Base(); - { - nested.a = "Nested"; - } - - public String getB() { - return b; - } - - public void setB(String b) { - this.b = b; - } - - public Base getNested() { - return nested; - } - - public void setNested(Base nested) { - this.nested = nested; - } - } - - @Test - void testReadWriteProperty() throws Exception { - Impl bean = new Impl(); - assertEquals("A",PropertyHelper.getProperty(bean, "a")); - assertEquals("B",PropertyHelper.getProperty(bean, "b")); - } - - @Test - void testReadOnlyProperty() throws Exception { - Impl bean = new Impl(); - assertEquals("C",PropertyHelper.getProperty(bean, "c")); - assertEquals(true,PropertyHelper.getProperty(bean, "d")); - } - - @Test - void testNestedProperty() throws Exception { - Impl bean = new Impl(); - assertEquals("Nested",PropertyHelper.getProperty(bean, "nested.a")); - } -} From ad566d1f60cae5ab57ea1cb3080efaa6983fc4bc Mon Sep 17 00:00:00 2001 From: Heiko Klare Date: Thu, 14 May 2026 11:46:59 +0200 Subject: [PATCH 13/19] Fix warnings in test plugins - Suppress warning for removal of tested class (test class already being deprecated as well) - Remove outdated and invalid entries from plugin.xml (referenced classes not being present anymore) - Fix path capitalization in plugin.xml --- .../tests/rules/DefaultPartitionerTest.java | 3 +++ tests/org.eclipse.ui.tests/plugin.xml | 17 +---------------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java index eb807c8ffcc..586e787494a 100644 --- a/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java +++ b/tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/rules/DefaultPartitionerTest.java @@ -21,11 +21,14 @@ @Deprecated public class DefaultPartitionerTest extends FastPartitionerTest { + @Deprecated + @SuppressWarnings("removal") @Override protected IDocumentPartitioner createPartitioner(IPartitionTokenScanner scanner) { return new DefaultPartitioner(scanner, new String[] { DEFAULT, COMMENT }); } + @Deprecated @Override @Test public void testPR130900() throws Exception { diff --git a/tests/org.eclipse.ui.tests/plugin.xml b/tests/org.eclipse.ui.tests/plugin.xml index eb2e3396754..5dbc053ba98 100644 --- a/tests/org.eclipse.ui.tests/plugin.xml +++ b/tests/org.eclipse.ui.tests/plugin.xml @@ -15,12 +15,6 @@ - - - - - + From bacf49321fe9041facd2f8ff85e41c5eaa08f42a Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Fri, 15 May 2026 14:15:41 +0000 Subject: [PATCH 14/19] Migrate OpenProjectExplorerFolderTest to JUnit 5 Migrate the JUnit 4 PerformanceTestCaseJunit4 base class to its JUnit 5 counterpart PerformanceTestCaseJunit5, which manages the PerformanceMeter via @BeforeEach/@AfterEach. --- .../performance/OpenProjectExplorerFolderTest.java | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java index fcef754b640..4ce3b7f548d 100644 --- a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/OpenProjectExplorerFolderTest.java @@ -13,7 +13,7 @@ *******************************************************************************/ package org.eclipse.ui.tests.performance; -import static org.junit.Assert.fail; +import static org.junit.jupiter.api.Assertions.fail; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -28,14 +28,14 @@ import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.Platform; -import org.eclipse.test.performance.PerformanceTestCaseJunit4; +import org.eclipse.test.performance.PerformanceTestCaseJunit5; import org.eclipse.ui.IViewPart; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.navigator.resources.ProjectExplorer; -import org.junit.ClassRule; -import org.junit.Test; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; import org.osgi.framework.Bundle; @@ -61,10 +61,10 @@ * "sleep" to simulate computations, it only effects Elapsed Time (not CPU * Time). */ -public class OpenProjectExplorerFolderTest extends PerformanceTestCaseJunit4 { +public class OpenProjectExplorerFolderTest extends PerformanceTestCaseJunit5 { - @ClassRule - public static final UIPerformanceTestRule uiPerformanceTestRule = new UIPerformanceTestRule(); + @RegisterExtension + static UIPerformanceTestRule uiPerformanceTestRule = new UIPerformanceTestRule(); /* * performance testcase for bug 106158 From bf35645d8b9efabc4705b6ee3433817d21e1876d Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 7 May 2026 15:14:42 +0200 Subject: [PATCH 15/19] Introduce internal CSS Selector AST and matcher Adds a small engine-internal selector AST (sealed Selector interface plus records for type, class, id, attribute, pseudo, compound, and combinator forms) and a static SelectorMatcher that walks an Element through it, alongside the existing SAC-based selector tree. New code only; nothing else in the engine consumes it yet. This is the foundation for Phase 3 step 1 of the CSS engine rework: once the parser-output translator and the engine.matches signature follow, the 26 vendored Batik selector wrappers under impl/sac/* can go away. Specificity follows CSS 2.1; pseudo-class semantics preserve the static-pseudo-instance carve-out from CSSPseudoClassConditionImpl so cascade behaviour does not shift. Sixteen unit tests cover universal, type case-sensitivity, class, id, compound, descendant, child, attribute presence and word-match, pseudo-class via isPseudoInstanceOf, selector lists, and CSS 2.1 specificity arithmetic. The adjacent-sibling test pins that the matcher returns false (rather than throwing) on elements without sibling support, since TestElement's ElementAdapter base does not expose siblings. The new package is exported x-friends to org.eclipse.e4.ui.tests.css.core so the unit tests can reach it. Bundle remains at JavaSE-17, so the matcher uses instanceof pattern chains rather than switch patterns. Contributes to #3980 --- .../META-INF/MANIFEST.MF | 1 + .../impl/engine/selector/SelectorMatcher.java | 263 ++++++++++++++++ .../core/impl/engine/selector/Selectors.java | 287 ++++++++++++++++++ .../engine/selector/SelectorMatcherTest.java | 223 ++++++++++++++ 4 files changed, 774 insertions(+) create mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java create mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java create mode 100644 tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java diff --git a/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF index 76e17ec59af..27dfb2acb81 100644 --- a/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF @@ -31,6 +31,7 @@ Export-Package: org.eclipse.e4.ui.css.core;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom.parsers;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom.properties;x-friends:="org.eclipse.e4.ui.css.swt", org.eclipse.e4.ui.css.core.impl.engine;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.swt", + org.eclipse.e4.ui.css.core.impl.engine.selector;x-friends:="org.eclipse.e4.ui.tests.css.core", org.eclipse.e4.ui.css.core.impl.sac;x-internal:=true, org.eclipse.e4.ui.css.core.resources;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.renderers.swt", org.eclipse.e4.ui.css.core.sac;x-internal:=true, diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java new file mode 100644 index 00000000000..602582f39a5 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java @@ -0,0 +1,263 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Adjacent; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.And; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeBeginHyphen; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeIncludes; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Child; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ClassSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Descendant; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ElementType; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.IdSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.PseudoClass; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Universal; +import org.w3c.dom.Element; +import org.w3c.dom.Node; + +/** + * Matches a {@link Selector} against an {@link Element}. + * + *

+ * Every method is static; the matcher carries no state. Callers pass the + * element being tested plus an optional pseudo-element string (the same + * argument the SAC engine carried) so that pseudo-class matching can defer + * to the existing {@link CSSStylableElement#isPseudoInstanceOf} contract. + *

+ * + *

+ * Tag-name comparison is case sensitive, matching the existing SAC matcher + * (Phase 1 test {@code testTagNameCaseSensitivity} in {@code CSSEngineTest} + * locks this in). Pseudo-class semantics also follow the existing engine: + * the static-pseudo-instance carve-out from + * {@code CSSPseudoClassConditionImpl} is preserved so cascade behaviour + * does not shift. + *

+ */ +public final class SelectorMatcher { + + private SelectorMatcher() { + // statics only + } + + /** + * @return {@code true} if {@code selector} matches {@code element} for + * the given pseudo state. + */ + public static boolean matches(Selector selector, Element element, String pseudoElement) { + if (element == null) { + return false; + } + if (selector instanceof Universal) { + return true; + } + if (selector instanceof ElementType type) { + return matchesElementType(type, element); + } + if (selector instanceof ClassSelector cls) { + return matchesClass(cls, element); + } + if (selector instanceof IdSelector id) { + return matchesId(id, element); + } + if (selector instanceof AttributeSelector attr) { + return matchesAttribute(attr, element); + } + if (selector instanceof AttributeIncludes inc) { + return matchesAttributeIncludes(inc, element); + } + if (selector instanceof AttributeBeginHyphen beg) { + return matchesAttributeBeginHyphen(beg, element); + } + if (selector instanceof PseudoClass pc) { + return matchesPseudoClass(pc, element, pseudoElement); + } + if (selector instanceof And and) { + return matches(and.left(), element, pseudoElement) && matches(and.right(), element, pseudoElement); + } + if (selector instanceof Descendant d) { + return matchesDescendant(d, element, pseudoElement); + } + if (selector instanceof Child c) { + return matchesChild(c, element, pseudoElement); + } + if (selector instanceof Adjacent a) { + return matchesAdjacent(a, element, pseudoElement); + } + if (selector instanceof SelectorList list) { + return matchesAny(list, element, pseudoElement); + } + throw new IllegalStateException("Unknown selector kind: " + selector.getClass()); //$NON-NLS-1$ + } + + private static boolean matchesElementType(ElementType type, Element element) { + String localName = type.localName(); + if (localName == null) { + return true; + } + String elementName = element.getPrefix() == null ? element.getNodeName() : element.getLocalName(); + return localName.equals(elementName); + } + + private static boolean matchesClass(ClassSelector cls, Element element) { + if (!(element instanceof CSSStylableElement stylable)) { + return false; + } + String elementClass = stylable.getCSSClass(); + if (elementClass == null) { + return false; + } + // CSS class attribute can be a whitespace-separated list of classes. + // Walk the string manually to avoid the regex compile + array allocation + // String.split forces on every match evaluation. + return containsWord(elementClass, cls.className()); + } + + private static boolean matchesId(IdSelector id, Element element) { + if (!(element instanceof CSSStylableElement stylable)) { + return false; + } + return id.id().equals(stylable.getCSSId()); + } + + private static boolean matchesAttribute(AttributeSelector attr, Element element) { + String name = attr.name(); + String required = attr.value(); + if (required == null) { + // presence form: [attr] + return element.hasAttribute(name); + } + String actual = element.getAttribute(name); + if (actual == null) { + return required.isEmpty(); + } + return required.equals(actual); + } + + private static boolean matchesAttributeIncludes(AttributeIncludes inc, Element element) { + String actual = element.getAttribute(inc.name()); + if (actual == null) { + return false; + } + return containsWord(actual, inc.value()); + } + + /** + * Returns {@code true} if {@code haystack} contains {@code word} as a + * whitespace-separated token. Equivalent to splitting on + * {@code \s+} and checking for an exact token match, but without the + * regex compile and array allocation each call. + */ + private static boolean containsWord(String haystack, String word) { + if (word == null || word.isEmpty()) { + return false; + } + int wordLength = word.length(); + int length = haystack.length(); + int i = 0; + while (i < length) { + while (i < length && Character.isWhitespace(haystack.charAt(i))) { + i++; + } + int start = i; + while (i < length && !Character.isWhitespace(haystack.charAt(i))) { + i++; + } + if (i - start == wordLength && haystack.regionMatches(start, word, 0, wordLength)) { + return true; + } + } + return false; + } + + private static boolean matchesAttributeBeginHyphen(AttributeBeginHyphen beg, Element element) { + String actual = element.getAttribute(beg.name()); + if (actual == null) { + return false; + } + String value = beg.value(); + return actual.equals(value) || actual.startsWith(value + "-"); + } + + private static boolean matchesPseudoClass(PseudoClass pseudo, Element element, String pseudoElement) { + String name = pseudo.name(); + // If the caller is iterating a static-pseudo cascade, only match the + // pseudo argument on the way down. + if (pseudoElement != null && !pseudoElement.equals(name)) { + return false; + } + if (!(element instanceof CSSStylableElement stylable)) { + return false; + } + if (!stylable.isPseudoInstanceOf(name)) { + return false; + } + if (pseudoElement == null) { + // Same carve-out as CSSPseudoClassConditionImpl: when no pseudo + // element argument is supplied, pseudos that the element + // publishes only as static instances do not match the regular + // cascade. They get applied separately via the default style + // declaration map. + return !stylable.isStaticPseudoInstance(name); + } + return true; + } + + private static boolean matchesDescendant(Descendant d, Element element, String pseudoElement) { + if (!matches(d.descendant(), element, pseudoElement)) { + return false; + } + Node parent = element.getParentNode(); + while (parent instanceof Element parentElement) { + if (matches(d.ancestor(), parentElement, null)) { + return true; + } + parent = parentElement.getParentNode(); + } + return false; + } + + private static boolean matchesChild(Child c, Element element, String pseudoElement) { + if (!matches(c.child(), element, pseudoElement)) { + return false; + } + Node parent = element.getParentNode(); + return parent instanceof Element parentElement && matches(c.parent(), parentElement, null); + } + + private static boolean matchesAdjacent(Adjacent a, Element element, String pseudoElement) { + if (!matches(a.second(), element, pseudoElement)) { + return false; + } + Node previous = element.getPreviousSibling(); + while (previous != null && !(previous instanceof Element)) { + previous = previous.getPreviousSibling(); + } + return previous instanceof Element previousElement && matches(a.first(), previousElement, null); + } + + private static boolean matchesAny(SelectorList list, Element element, String pseudoElement) { + for (Selector alternative : list.alternatives()) { + if (matches(alternative, element, pseudoElement)) { + return true; + } + } + return false; + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java new file mode 100644 index 00000000000..552163a3a19 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java @@ -0,0 +1,287 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import java.util.List; + +/** + * Internal CSS selector AST. + * + *

+ * The engine historically exposed W3C SAC selectors + * ({@code org.w3c.css.sac.Selector} and friends) and matched against them + * through a hierarchy of vendored Batik wrapper classes under + * {@code impl/sac/*}. This package replaces both with a small set of records + * that the engine owns end to end. The W3C SAC types stay only as long as + * the parser still emits them; a translator turns the SAC selector tree + * produced by the Batik SAC parser into one of these records before it + * reaches the engine matcher. + *

+ * + *

+ * Specificity follows CSS 2.1: 100 per id, 10 per class / attribute / + * pseudo-class, 1 per element, 0 for the universal selector. Combinators + * sum the specificity of their operands; selector lists report the maximum + * specificity over their alternatives. + *

+ */ +public final class Selectors { + + private Selectors() { + // constants only + } + + /** A parsed CSS selector. Sealed; pattern-match in the matcher. */ + public sealed interface Selector + permits Universal, ElementType, ClassSelector, IdSelector, AttributeSelector, + AttributeIncludes, AttributeBeginHyphen, PseudoClass, And, Descendant, Child, Adjacent, SelectorList { + + /** CSS specificity contribution of this selector. */ + int specificity(); + + /** Best-effort textual reproduction of the selector. */ + String text(); + } + + /** {@code *} — matches any element. */ + public record Universal() implements Selector { + @Override + public int specificity() { + return 0; + } + + @Override + public String text() { + return "*"; + } + } + + /** {@code Button} — matches by local element name. */ + public record ElementType(String localName) implements Selector { + @Override + public int specificity() { + return 1; + } + + @Override + public String text() { + return localName; + } + } + + /** {@code .foo} — matches by CSS class. */ + public record ClassSelector(String className) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return "." + className; + } + } + + /** {@code #foo} — matches by CSS id. */ + public record IdSelector(String id) implements Selector { + @Override + public int specificity() { + return 100; + } + + @Override + public String text() { + return "#" + id; + } + } + + /** + * {@code [attr]} or {@code [attr='value']}. {@code value} is {@code null} + * for the presence form and the empty string for {@code [attr='']}. + */ + public record AttributeSelector(String name, String value) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return value == null ? "[" + name + "]" : "[" + name + "='" + value + "']"; + } + } + + /** {@code [attr~='value']} — matches when {@code attr} contains {@code value} as a whitespace-separated word. */ + public record AttributeIncludes(String name, String value) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return "[" + name + "~='" + value + "']"; + } + } + + /** {@code [attr|='value']} — matches when {@code attr} equals {@code value} or starts with {@code value-}. */ + public record AttributeBeginHyphen(String name, String value) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return "[" + name + "|='" + value + "']"; + } + } + + /** + * {@code :name} — matches when the element answers true to + * {@code isPseudoInstanceOf(name)} on its CSS-stylable element wrapper. + */ + public record PseudoClass(String name) implements Selector { + @Override + public int specificity() { + return 10; + } + + @Override + public String text() { + return ":" + name; + } + } + + /** + * Compound selector: every operand must match the same element. Built + * by the translator for forms like {@code Button.primary#go} or + * {@code [a][b]} where multiple simple selectors apply to one element. + */ + public record And(Selector left, Selector right) implements Selector { + @Override + public int specificity() { + return left.specificity() + right.specificity(); + } + + @Override + public String text() { + return left.text() + right.text(); + } + } + + /** {@code ancestor descendant} — descendant combinator. */ + public record Descendant(Selector ancestor, Selector descendant) implements Selector { + @Override + public int specificity() { + return ancestor.specificity() + descendant.specificity(); + } + + @Override + public String text() { + return ancestor.text() + " " + descendant.text(); + } + } + + /** {@code parent > child} — child combinator. */ + public record Child(Selector parent, Selector child) implements Selector { + @Override + public int specificity() { + return parent.specificity() + child.specificity(); + } + + @Override + public String text() { + return parent.text() + " > " + child.text(); + } + } + + /** {@code first + second} — direct adjacent sibling combinator. */ + public record Adjacent(Selector first, Selector second) implements Selector { + @Override + public int specificity() { + return first.specificity() + second.specificity(); + } + + @Override + public String text() { + return first.text() + " + " + second.text(); + } + } + + /** + * {@code a, b} — selector list. Specificity reports the maximum over the + * alternatives so cascade ordering can match a list against an element by + * iterating its alternatives. + * + *

+ * A regular final class rather than a record because the cascade reads + * {@link #specificity()} once per matched alternative and we want it + * precomputed; record components cannot host derived state. + *

+ */ + public static final class SelectorList implements Selector { + + private final List alternatives; + private final int specificity; + + public SelectorList(List alternatives) { + this.alternatives = List.copyOf(alternatives); + int max = 0; + for (Selector alternative : this.alternatives) { + int s = alternative.specificity(); + if (s > max) { + max = s; + } + } + this.specificity = max; + } + + public List alternatives() { + return alternatives; + } + + @Override + public int specificity() { + return specificity; + } + + @Override + public String text() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < alternatives.size(); i++) { + if (i > 0) { + sb.append(", "); //$NON-NLS-1$ + } + sb.append(alternatives.get(i).text()); + } + return sb.toString(); + } + + @Override + public boolean equals(Object o) { + return o instanceof SelectorList other && alternatives.equals(other.alternatives); + } + + @Override + public int hashCode() { + return alternatives.hashCode(); + } + + @Override + public String toString() { + return text(); + } + } +} diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java new file mode 100644 index 00000000000..31162dd52c7 --- /dev/null +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcherTest.java @@ -0,0 +1,223 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +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.List; + +import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Adjacent; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.And; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeBeginHyphen; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeIncludes; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Child; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ClassSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Descendant; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ElementType; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.IdSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.PseudoClass; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Universal; +import org.eclipse.e4.ui.tests.css.core.util.TestElement; +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link SelectorMatcher}. The cases mirror those in the + * Phase 1 {@code CSSEngineTest}, but go through the new internal selector + * AST instead of SAC. When Phase 3 Step 1 wires the engine to use this + * matcher, the SAC-based duplicate tests can be retired. + */ +class SelectorMatcherTest { + + private static final TestCSSEngine ENGINE = new TestCSSEngine(); + + private static final class TestCSSEngine extends CSSEngineImpl { + @Override + public void reapply() { + } + } + + private static TestElement element(String tag, String cssClass, String id) { + TestElement e = new TestElement(tag, ENGINE); + if (cssClass != null) { + e.setClass(cssClass); + } + if (id != null) { + e.setId(id); + } + return e; + } + + @Test + void universalMatchesAnything() { + assertTrue(SelectorMatcher.matches(new Universal(), element("Button", null, null), null)); + assertTrue(SelectorMatcher.matches(new Universal(), element("Label", "x", "y"), null)); + } + + @Test + void typeSelectorIsCaseSensitive() { + assertTrue(SelectorMatcher.matches(new ElementType("Button"), element("Button", null, null), null)); + assertFalse(SelectorMatcher.matches(new ElementType("Button"), element("button", null, null), null)); + assertFalse(SelectorMatcher.matches(new ElementType("Button"), element("Label", null, null), null)); + } + + @Test + void classSelector() { + assertTrue(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", "foo", null), null)); + assertFalse(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", "bar", null), null)); + assertFalse(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", null, null), null)); + } + + @Test + void classSelectorMatchesOneOfMultipleClasses() { + assertTrue(SelectorMatcher.matches(new ClassSelector("foo"), element("Button", "foo bar", null), null)); + assertTrue(SelectorMatcher.matches(new ClassSelector("bar"), element("Button", "foo bar", null), null)); + assertFalse(SelectorMatcher.matches(new ClassSelector("baz"), element("Button", "foo bar", null), null)); + } + + @Test + void idSelector() { + assertTrue(SelectorMatcher.matches(new IdSelector("go"), element("Button", null, "go"), null)); + assertFalse(SelectorMatcher.matches(new IdSelector("go"), element("Button", null, "stop"), null)); + assertFalse(SelectorMatcher.matches(new IdSelector("go"), element("Button", null, null), null)); + } + + @Test + void compoundSelector() { + Selectors.Selector selector = new And(new And(new ElementType("Button"), new ClassSelector("primary")), + new IdSelector("go")); + assertTrue(SelectorMatcher.matches(selector, element("Button", "primary", "go"), null)); + assertFalse(SelectorMatcher.matches(selector, element("Label", "primary", "go"), null)); + assertFalse(SelectorMatcher.matches(selector, element("Button", "secondary", "go"), null)); + assertFalse(SelectorMatcher.matches(selector, element("Button", "primary", "stop"), null)); + } + + @Test + void descendantCombinator() { + Selectors.Selector selector = new Descendant(new ElementType("Composite"), new ElementType("Button")); + TestElement composite = element("Composite", null, null); + TestElement intermediate = new TestElement("Group", composite, ENGINE); + TestElement button = new TestElement("Button", intermediate, ENGINE); + assertTrue(SelectorMatcher.matches(selector, button, null)); + + TestElement orphan = element("Button", null, null); + assertFalse(SelectorMatcher.matches(selector, orphan, null)); + } + + @Test + void childCombinator() { + Selectors.Selector selector = new Child(new ElementType("Composite"), new ElementType("Button")); + TestElement composite = element("Composite", null, null); + TestElement direct = new TestElement("Button", composite, ENGINE); + assertTrue(SelectorMatcher.matches(selector, direct, null)); + + TestElement intermediate = new TestElement("Group", composite, ENGINE); + TestElement grandchild = new TestElement("Button", intermediate, ENGINE); + assertFalse(SelectorMatcher.matches(selector, grandchild, null)); + } + + @Test + void attributePresentMatchesEvenWithEmptyValue() { + AttributeSelector selector = new AttributeSelector("style", null); + TestElement withAttr = element("Button", null, null); + withAttr.setAttribute("style", "SWT.PUSH"); + assertTrue(SelectorMatcher.matches(selector, withAttr, null)); + assertFalse(SelectorMatcher.matches(selector, element("Button", null, null), null)); + } + + @Test + void attributeIncludesIsWordBoundaryMatch() { + AttributeIncludes selector = new AttributeIncludes("style", "SWT.CHECK"); + TestElement match = element("Button", null, null); + match.setAttribute("style", "SWT.CHECK SWT.BORDER"); + assertTrue(SelectorMatcher.matches(selector, match, null)); + + TestElement substring = element("Button", null, null); + substring.setAttribute("style", "SWT.CHECK_DELAYED"); + // 'CHECK' is a substring of 'CHECK_DELAYED' but not a whitespace-separated word. + assertFalse(SelectorMatcher.matches(new AttributeIncludes("style", "CHECK"), substring, null)); + } + + @Test + void attributeBeginHyphen() { + AttributeBeginHyphen selector = new AttributeBeginHyphen("lang", "en"); + TestElement exact = element("p", null, null); + exact.setAttribute("lang", "en"); + TestElement prefixed = element("p", null, null); + prefixed.setAttribute("lang", "en-US"); + TestElement other = element("p", null, null); + other.setAttribute("lang", "fr"); + assertTrue(SelectorMatcher.matches(selector, exact, null)); + assertTrue(SelectorMatcher.matches(selector, prefixed, null)); + assertFalse(SelectorMatcher.matches(selector, other, null)); + } + + @Test + void pseudoClassMatchesViaIsPseudoInstanceOf() { + PseudoClass selector = new PseudoClass("selected"); + TestElement on = new TestElement("Button", ENGINE) { + @Override + public boolean isPseudoInstanceOf(String s) { + return "selected".equals(s); + } + }; + TestElement off = element("Button", null, null); + assertTrue(SelectorMatcher.matches(selector, on, null)); + assertFalse(SelectorMatcher.matches(selector, off, null)); + } + + @Test + void selectorListMatchesAnyAlternative() { + SelectorList list = new SelectorList(List.of(new ClassSelector("a"), new ClassSelector("b"))); + assertTrue(SelectorMatcher.matches(list, element("Button", "a", null), null)); + assertTrue(SelectorMatcher.matches(list, element("Button", "b", null), null)); + assertFalse(SelectorMatcher.matches(list, element("Button", "c", null), null)); + } + + @Test + void specificityMatchesCss21() { + assertEquals(0, new Universal().specificity()); + assertEquals(1, new ElementType("Button").specificity()); + assertEquals(10, new ClassSelector("foo").specificity()); + assertEquals(100, new IdSelector("go").specificity()); + assertEquals(11, new And(new ElementType("Button"), new ClassSelector("primary")).specificity()); + assertEquals(111, new And(new And(new ElementType("Button"), new ClassSelector("primary")), + new IdSelector("go")).specificity()); + } + + @Test + void specificityOfSelectorListIsMaxOverAlternatives() { + // "Button, .foo, #go": max specificity is the id (100). + SelectorList list = new SelectorList( + List.of(new ElementType("Button"), new ClassSelector("foo"), new IdSelector("go"))); + assertEquals(100, list.specificity()); + } + + @Test + void adjacentSiblingCombinatorRequiresSiblingSupport() { + // TestElement's ElementAdapter base returns null from getPreviousSibling, + // so adjacent matching cannot succeed against it. Locks in that the + // matcher returns false rather than throwing on elements without + // sibling support. + Adjacent selector = new Adjacent(new ElementType("Label"), new ElementType("Button")); + TestElement parent = element("Composite", null, null); + new TestElement("Label", parent, ENGINE); + TestElement second = new TestElement("Button", parent, ENGINE); + assertFalse(SelectorMatcher.matches(selector, second, null)); + } +} From ee654373bd66806c6ce1e8d79792f9a4fa7eb294 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 7 May 2026 16:16:58 +0200 Subject: [PATCH 16/19] Drop SAC types from the CSS engine API and matcher Adds a SAC->internal Selector translator at the parser-output boundary in CSSDocumentHandlerImpl.startSelector and switches CSSEngine.matches and parseSelectors to the internal Selectors AST. CSSEngineImpl.matches now delegates to SelectorMatcher; applyConditionalPseudoStyle walks the internal AST instead of the SAC tree. The parser is configured with Batik's stock DefaultSelectorFactory and DefaultConditionFactory, so 23 vendored impl/sac/* selector and condition wrappers fall away. The dead ExtendedDocumentCSS.queryConditionSelector / querySelector methods (and the SAC_*_CONDITION int constants behind them) go with them. Selectors.SelectorList grows getLength()/item(int) SAC-style accessors and every record overrides toString() to return text() for readable selector text in error messages and rule.getSelectorText(). CSSEngineTest and SelectorTest are rewritten on the internal AST. Phase 3 step 1 of the CSS engine rework. Net ~-2,300 LOC. Three impl/sac classes (CSSDocumentHandlerImpl, DocumentHandlerFactoryImpl, SACParserFactoryImpl) remain as parser plumbing for step 2. Contributes to #3980 --- .../e4/ui/css/core/dom/ExtendedCSSRule.java | 9 +- .../ui/css/core/dom/ExtendedDocumentCSS.java | 16 +- .../e4/ui/css/core/engine/CSSEngine.java | 13 +- .../css/core/impl/dom/CSSStyleRuleImpl.java | 23 +- .../ui/css/core/impl/dom/DocumentCSSImpl.java | 103 +-------- .../e4/ui/css/core/impl/dom/ViewCSSImpl.java | 65 +++--- .../css/core/impl/engine/CSSEngineImpl.java | 176 +++++++++------ .../impl/engine/selector/SacTranslator.java | 160 +++++++++++++ .../impl/engine/selector/SelectorMatcher.java | 81 ++++--- .../core/impl/engine/selector/Selectors.java | 90 +++++++- .../impl/sac/AbstractAttributeCondition.java | 86 ------- .../impl/sac/AbstractCombinatorCondition.java | 93 -------- .../impl/sac/AbstractDescendantSelector.java | 93 -------- .../impl/sac/AbstractElementSelector.java | 87 ------- .../impl/sac/AbstractSiblingSelector.java | 108 --------- .../core/impl/sac/CSSAndConditionImpl.java | 73 ------ .../impl/sac/CSSAttributeConditionImpl.java | 153 ------------- .../CSSBeginHyphenAttributeConditionImpl.java | 64 ------ .../core/impl/sac/CSSChildSelectorImpl.java | 102 --------- .../core/impl/sac/CSSClassConditionImpl.java | 65 ------ .../impl/sac/CSSConditionFactoryImpl.java | 213 ------------------ .../impl/sac/CSSConditionalSelectorImpl.java | 135 ----------- .../impl/sac/CSSDescendantSelectorImpl.java | 109 --------- .../sac/CSSDirectAdjacentSelectorImpl.java | 106 --------- .../core/impl/sac/CSSDocumentHandlerImpl.java | 6 +- .../core/impl/sac/CSSElementSelectorImpl.java | 98 -------- .../css/core/impl/sac/CSSIdConditionImpl.java | 129 ----------- .../core/impl/sac/CSSLangConditionImpl.java | 117 ---------- .../sac/CSSOneOfAttributeConditionImpl.java | 75 ------ .../impl/sac/CSSPseudoClassConditionImpl.java | 145 ------------ .../sac/CSSPseudoElementSelectorImpl.java | 69 ------ .../core/impl/sac/CSSSelectorFactoryImpl.java | 182 --------------- .../css/core/impl/sac/ExtendedCondition.java | 47 ---- .../css/core/impl/sac/ExtendedSelector.java | 51 ----- .../ui/css/swt/engine/CSSSWTEngineImpl.java | 25 +- .../e4/ui/tests/css/core/CSSEngineTest.java | 14 +- .../tests/css/core/parser/SelectorTest.java | 24 +- 37 files changed, 489 insertions(+), 2716 deletions(-) create mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SacTranslator.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java delete mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java index ae6bd4c11f5..1d50bb853ed 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedCSSRule.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2013 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -13,8 +13,7 @@ *******************************************************************************/ package org.eclipse.e4.ui.css.core.dom; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.css.CSSRule; /** @@ -28,7 +27,7 @@ public interface ExtendedCSSRule extends CSSRule { public CSSPropertyList getCSSPropertyList(); /** - * Return the list of {@link Selector} of this {@link CSSRule}. + * Return the list of selectors of this {@link CSSRule}. */ - public SelectorList getSelectorList(); + public Selectors.SelectorList getSelectorList(); } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java index f23d30d74cf..d3d7bc717d1 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/dom/ExtendedDocumentCSS.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2018 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -15,9 +15,6 @@ package org.eclipse.e4.ui.css.core.dom; import java.util.EventListener; -import java.util.List; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.Selector; import org.w3c.dom.css.DocumentCSS; import org.w3c.dom.stylesheets.StyleSheet; @@ -26,21 +23,10 @@ */ public interface ExtendedDocumentCSS extends DocumentCSS { - public static final Integer SAC_ID_CONDITION = Integer.valueOf(Condition.SAC_ID_CONDITION); - public static final Integer SAC_CLASS_CONDITION = Integer.valueOf(Condition.SAC_CLASS_CONDITION); - public static final Integer SAC_PSEUDO_CLASS_CONDITION = Integer.valueOf(Condition.SAC_PSEUDO_CLASS_CONDITION); - public static final Integer OTHER_SAC_CONDITIONAL_SELECTOR = Integer.valueOf(Selector.SAC_CONDITIONAL_SELECTOR); - - public static final Integer OTHER_SAC_SELECTOR = Integer.valueOf(999); - public void addStyleSheet(StyleSheet styleSheet); public void removeAllStyleSheets(); - public List queryConditionSelector(int conditionType); - - public List querySelector(int selectorType, int conditionType); - /** * @since 0.12.200 */ diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java index a6624b84852..f036cf16154 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/engine/CSSEngine.java @@ -20,11 +20,10 @@ import org.eclipse.e4.ui.css.core.dom.IElementProvider; import org.eclipse.e4.ui.css.core.dom.properties.ICSSPropertyHandler; import org.eclipse.e4.ui.css.core.dom.properties.converters.ICSSValueConverter; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; import org.w3c.css.sac.InputSource; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; import org.w3c.dom.css.CSSStyleDeclaration; import org.w3c.dom.css.CSSStyleSheet; @@ -105,27 +104,27 @@ public interface CSSEngine { /** * Parse Selectors from String value. */ - SelectorList parseSelectors(String text); + Selectors.SelectorList parseSelectors(String text); /** * Parse Selectors from InputSource value. */ - SelectorList parseSelectors(InputSource source) throws IOException; + Selectors.SelectorList parseSelectors(InputSource source) throws IOException; /** * Parse Selectors from InputStream. */ - SelectorList parseSelectors(InputStream stream) throws IOException; + Selectors.SelectorList parseSelectors(InputStream stream) throws IOException; /** * Parse Selectors from String value. */ - SelectorList parseSelectors(Reader reader) throws IOException; + Selectors.SelectorList parseSelectors(Reader reader) throws IOException; /** * Check if the selector matches the object node. */ - boolean matches(Selector selector, Object node, String pseudo); + boolean matches(Selectors.Selector selector, Object node, String pseudo); /*--------------- Apply styles -----------------*/ diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java index 01e5822bd4a..23200bae6c2 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/CSSStyleRuleImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2015 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -17,8 +17,7 @@ import org.eclipse.e4.ui.css.core.dom.CSSPropertyList; import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.DOMException; import org.w3c.dom.css.CSSRule; import org.w3c.dom.css.CSSStyleDeclaration; @@ -27,10 +26,10 @@ public class CSSStyleRuleImpl extends CSSRuleImpl implements CSSStyleRule, ExtendedCSSRule { - private final SelectorList selectors; + private final Selectors.SelectorList selectors; private CSSStyleDeclaration styleDeclaration; - public CSSStyleRuleImpl(CSSStyleSheet parentStyleSheet, CSSRule parentRule, SelectorList selectors) { + public CSSStyleRuleImpl(CSSStyleSheet parentStyleSheet, CSSRule parentRule, Selectors.SelectorList selectors) { super(parentStyleSheet, parentRule); this.selectors = selectors; } @@ -55,17 +54,7 @@ public String getCssText() { @Override public String getSelectorText() { - StringBuilder sb = new StringBuilder(); - for (int selID = 0; selID < getSelectorList().getLength(); selID++) { - Selector item = getSelectorList().item(selID); - sb.append(item.toString()); - sb.append(", "); - } - if (getSelectorList().getLength() > 0) { - sb.delete(sb.length() - 2, sb.length()); - } - - return sb.toString(); + return selectors.text(); } @Override @@ -83,7 +72,7 @@ public void setSelectorText(String selectorText) throws DOMException { // Additional methods @Override - public SelectorList getSelectorList() { + public Selectors.SelectorList getSelectorList() { return selectors; } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java index f9fba83d961..b87b42b143c 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/DocumentCSSImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2018 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -18,19 +18,10 @@ package org.eclipse.e4.ui.css.core.impl.dom; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; -import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; -import org.w3c.dom.css.CSSRule; -import org.w3c.dom.css.CSSRuleList; import org.w3c.dom.css.CSSStyleDeclaration; -import org.w3c.dom.css.CSSStyleSheet; import org.w3c.dom.css.DocumentCSS; import org.w3c.dom.stylesheets.StyleSheet; import org.w3c.dom.stylesheets.StyleSheetList; @@ -42,11 +33,6 @@ public class DocumentCSSImpl implements ExtendedDocumentCSS { private final StyleSheetListImpl styleSheetList = new StyleSheetListImpl(); - /** - * key=selector type, value = CSSStyleDeclaration - */ - private Map> styleDeclarationMap; - private final List styleSheetChangeListeners = new ArrayList<>(1); @Override @@ -72,93 +58,6 @@ public void removeAllStyleSheets() { styleSheetChangeListeners.forEach(l -> l.styleSheetRemoved(styleSheet)); } styleSheetList.removeAllStyleSheets(); - this.styleDeclarationMap = null; - } - - @Override - public List queryConditionSelector(int conditionType) { - return querySelector(Selector.SAC_CONDITIONAL_SELECTOR, conditionType); - } - - @Override - public List querySelector(int selectorType, int conditionType) { - List list = getCSSStyleDeclarationList(selectorType, conditionType); - if (list != null) { - return list; - } - int l = styleSheetList.getLength(); - for (int i = 0; i < l; i++) { - CSSStyleSheet styleSheet = (CSSStyleSheet) styleSheetList.item(i); - CSSRuleList ruleList = styleSheet.getCssRules(); - list = querySelector(ruleList, selectorType, conditionType); - setCSSStyleDeclarationList(list, selectorType, conditionType); - } - return list; - } - - protected List querySelector(CSSRuleList ruleList, int selectorType, int selectorConditionType) { - List list = new ArrayList<>(); - if (selectorType == Selector.SAC_CONDITIONAL_SELECTOR) { - int length = ruleList.getLength(); - for (int i = 0; i < length; i++) { - CSSRule rule = ruleList.item(i); - if (rule.getType() == CSSRule.STYLE_RULE && rule instanceof ExtendedCSSRule r) { - SelectorList selectorList = r.getSelectorList(); - // Loop for SelectorList - int l = selectorList.getLength(); - for (int j = 0; j < l; j++) { - Selector selector = selectorList.item(j); - if (selector.getSelectorType() == selectorType) { - // It's conditional selector - ConditionalSelector conditionalSelector = (ConditionalSelector) selector; - short conditionType = conditionalSelector.getCondition().getConditionType(); - if (selectorConditionType == conditionType) { - // current selector match the current CSS - // Rule - // CSSStyleRule styleRule = (CSSStyleRule) - // rule; - list.add(selector); - } - } - } - } - } - } - return list; - } - - protected List getCSSStyleDeclarationList(int selectorType, int conditionType) { - Integer key = getKey(selectorType, conditionType); - return getStyleDeclarationMap().get(key); - } - - protected void setCSSStyleDeclarationList(List list, int selectorType, int conditionType) { - Integer key = getKey(selectorType, conditionType); - getStyleDeclarationMap().put(key, list); - } - - protected Integer getKey(int selectorType, int conditionType) { - if (selectorType == Selector.SAC_CONDITIONAL_SELECTOR) { - if (conditionType == SAC_CLASS_CONDITION.intValue()) { - return SAC_CLASS_CONDITION; - } - if (conditionType == SAC_ID_CONDITION.intValue()) { - return SAC_ID_CONDITION; - } - if (conditionType == SAC_PSEUDO_CLASS_CONDITION.intValue()) { - return SAC_PSEUDO_CLASS_CONDITION; - } - return OTHER_SAC_CONDITIONAL_SELECTOR; - } - - return OTHER_SAC_SELECTOR; - } - - protected Map> getStyleDeclarationMap() { - if (styleDeclarationMap == null) { - styleDeclarationMap = new HashMap<>(); - } - return styleDeclarationMap; } @Override diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java index c628ce9cf41..aa1a5c8a92c 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2008, 2018 Angelo Zerr and others. + * Copyright (c) 2008, 2026 Angelo Zerr and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -21,11 +21,9 @@ import java.util.List; import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; -import org.eclipse.e4.ui.css.core.impl.sac.ExtendedSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SelectorMatcher; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.Element; -import org.w3c.dom.Node; import org.w3c.dom.css.CSSRule; import org.w3c.dom.css.CSSRuleList; import org.w3c.dom.css.CSSStyleDeclaration; @@ -112,47 +110,42 @@ private List getCombinedRules() { } private CSSStyleDeclaration getComputedStyle(List ruleList, Element elt, String pseudoElt) { - Node parent = elt.getParentNode(); - - Node[] hierarchy = null; - if (parent != null) { - List hierarchyList = new ArrayList<>(); - for (Node n = parent; n != null; n = n.getParentNode()) { - hierarchyList.add(n); - } - hierarchy = hierarchyList.toArray(new Node[hierarchyList.size()]); - } - List styleDeclarations = null; StyleWrapper firstStyleDeclaration = null; int position = 0; + + int depth = 0; + for (org.w3c.dom.Node n = elt; n instanceof Element; n = n.getParentNode()) { + depth++; + } + Element[] hierarchy = new Element[depth]; + int idx = 0; + for (org.w3c.dom.Node n = elt; n instanceof Element; n = n.getParentNode()) { + hierarchy[idx++] = (Element) n; + } + for (CSSRule rule : ruleList) { if (rule.getType() != CSSRule.STYLE_RULE || (!(rule instanceof ExtendedCSSRule)) ) { continue; // we only handle the CSSRule.STYLE_RULE and ExtendedCSSRule case } CSSStyleRule styleRule = (CSSStyleRule) rule; ExtendedCSSRule r = (ExtendedCSSRule) rule; - SelectorList selectorList = r.getSelectorList(); - // Loop for SelectorList - int l = selectorList.getLength(); - for (int j = 0; j < l; j++) { - Selector selector = selectorList.item(j); - if (selector instanceof ExtendedSelector extendedSelector) { - if (extendedSelector.match(elt, hierarchy, 0, pseudoElt)) { - CSSStyleDeclaration style = styleRule.getStyle(); - int specificity = extendedSelector.getSpecificity(); - StyleWrapper wrapper = new StyleWrapper(style, specificity, position++); - if (firstStyleDeclaration == null) { - firstStyleDeclaration = wrapper; - } else { - // There is several Style Declarations which - // match the current element - if (styleDeclarations == null) { - styleDeclarations = new ArrayList<>(); - styleDeclarations.add(firstStyleDeclaration); - } - styleDeclarations.add(wrapper); + Selectors.SelectorList selectorList = r.getSelectorList(); + for (Selectors.Selector selector : selectorList.alternatives()) { + if (SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0)) { + CSSStyleDeclaration style = styleRule.getStyle(); + int specificity = selector.specificity(); + StyleWrapper wrapper = new StyleWrapper(style, specificity, position++); + if (firstStyleDeclaration == null) { + firstStyleDeclaration = wrapper; + } else { + // There is several Style Declarations which + // match the current element + if (styleDeclarations == null) { + styleDeclarations = new ArrayList<>(); + styleDeclarations.add(firstStyleDeclaration); } + styleDeclarations.add(wrapper); } } } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java index 2fb2d1829de..66e7ede4f84 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java @@ -62,23 +62,17 @@ import org.eclipse.e4.ui.css.core.impl.dom.CSSStyleSheetImpl; import org.eclipse.e4.ui.css.core.impl.dom.DocumentCSSImpl; import org.eclipse.e4.ui.css.core.impl.dom.ViewCSSImpl; -import org.eclipse.e4.ui.css.core.impl.sac.CSSConditionFactoryImpl; -import org.eclipse.e4.ui.css.core.impl.sac.CSSSelectorFactoryImpl; -import org.eclipse.e4.ui.css.core.impl.sac.ExtendedSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SacTranslator; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SelectorMatcher; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.eclipse.e4.ui.css.core.resources.IResourcesRegistry; import org.eclipse.e4.ui.css.core.resources.ResourceRegistryKeyFactory; import org.eclipse.e4.ui.css.core.util.impl.resources.ResourcesLocatorManager; import org.eclipse.e4.ui.css.core.util.resources.IResourcesLocatorManager; import org.eclipse.e4.ui.css.core.utils.StringUtils; -import org.w3c.css.sac.AttributeCondition; -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionFactory; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.DescendantSelector; +import org.apache.batik.css.parser.DefaultConditionFactory; +import org.apache.batik.css.parser.DefaultSelectorFactory; import org.w3c.css.sac.InputSource; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -105,9 +99,6 @@ */ public abstract class CSSEngineImpl implements CSSEngine { - public static final ConditionFactory CONDITIONFACTORY_INSTANCE = new CSSConditionFactoryImpl( - null, "class", null, "id"); - /** * Archives are deliberately identified by exclamation mark in URLs */ @@ -324,7 +315,7 @@ public CSSStyleDeclaration parseStyleDeclaration(InputSource source) throws IOEx /*--------------- Parse CSS Selector -----------------*/ @Override - public SelectorList parseSelectors(String selector) { + public Selectors.SelectorList parseSelectors(String selector) { try { return parseSelectors(new StringReader(selector)); } catch (IOException e) { @@ -333,24 +324,24 @@ public SelectorList parseSelectors(String selector) { } @Override - public SelectorList parseSelectors(Reader reader) throws IOException { + public Selectors.SelectorList parseSelectors(Reader reader) throws IOException { InputSource source = new InputSource(); source.setCharacterStream(reader); return parseSelectors(source); } @Override - public SelectorList parseSelectors(InputStream stream) throws IOException { + public Selectors.SelectorList parseSelectors(InputStream stream) throws IOException { InputSource source = new InputSource(); source.setByteStream(stream); return parseSelectors(source); } @Override - public SelectorList parseSelectors(InputSource source) throws IOException { + public Selectors.SelectorList parseSelectors(InputSource source) throws IOException { checkInputSource(source); CSSParser parser = makeCSSParser(); - return parser.parseSelectors(source); + return SacTranslator.translate(parser.parseSelectors(source)); } /*--------------- Parse CSS Property Value-----------------*/ @@ -387,6 +378,28 @@ public CSSValue parsePropertyValue(InputSource source) throws IOException { /*--------------- Apply styles -----------------*/ + private final ThreadLocal> styledElements = new ThreadLocal<>(); + + public void startStylingSession() { + styledElements.set(new HashSet<>()); + } + + public void stopStylingSession() { + styledElements.remove(); + } + + public boolean isElementStyled(Object element) { + Set set = styledElements.get(); + return set != null && set.contains(element); + } + + public void markElementStyled(Object element) { + Set set = styledElements.get(); + if (set != null) { + set.add(element); + } + } + @Override public void applyStyles(Object element, boolean applyStylesToChildNodes) { applyStyles(element, applyStylesToChildNodes, computeDefaultStyle); @@ -399,6 +412,21 @@ public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean return; } + if (isElementStyled(element)) { + if (applyStylesToChildNodes) { + NodeList nodes = elt instanceof ChildVisibilityAwareElement c + ? c.getVisibleChildNodes() + : elt.getChildNodes(); + if (nodes != null) { + processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); + onStylesAppliedToChildNodes(elt, nodes); + } + } + return; + } + + markElementStyled(element); + /* * Compute new Style to apply. */ @@ -491,46 +519,57 @@ protected boolean isVisible(Element elt) { return true; } - private void applyConditionalPseudoStyle(ExtendedCSSRule parentRule, String pseudoInstance, Object element, CSSStyleDeclaration styleWithPseudoInstance) { - SelectorList selectorList = parentRule.getSelectorList(); - for (int j = 0; j < selectorList.getLength(); j++) { - Selector item = selectorList.item(j); - // search for conditional selectors - ConditionalSelector conditional = null; - if (item instanceof ConditionalSelector) { - conditional = (ConditionalSelector) item; - } else if (item instanceof DescendantSelector) { - if (((DescendantSelector) item).getSimpleSelector() instanceof ConditionalSelector) { - conditional = (ConditionalSelector) ((DescendantSelector) item).getSimpleSelector(); - } else if (((DescendantSelector) item).getAncestorSelector() instanceof ConditionalSelector) { - conditional = (ConditionalSelector) ((DescendantSelector) item).getAncestorSelector(); - } - } - if (conditional != null) { - Condition condition = conditional.getCondition(); - // we're only interested in attribute selector conditions - AttributeCondition attr = null; - if (condition instanceof AttributeCondition) { - attr = (AttributeCondition) condition; - } else if (condition instanceof CombinatorCondition) { - if (((CombinatorCondition) condition).getSecondCondition() instanceof AttributeCondition) { - attr = (AttributeCondition) ((CombinatorCondition) condition).getSecondCondition(); - } else if (((CombinatorCondition) condition).getFirstCondition() instanceof AttributeCondition) { - attr = (AttributeCondition) ((CombinatorCondition) condition).getFirstCondition(); - } - } - if (attr != null) { - String value = attr.getValue(); - if (value.equals(pseudoInstance)) { - // if we match the pseudo, apply the style - applyStyleDeclaration(element, styleWithPseudoInstance, pseudoInstance); - return; - } - } + private void applyConditionalPseudoStyle(ExtendedCSSRule parentRule, String pseudoInstance, Object element, + CSSStyleDeclaration styleWithPseudoInstance) { + Selectors.SelectorList selectorList = parentRule.getSelectorList(); + for (Selectors.Selector alternative : selectorList.alternatives()) { + if (matchesPseudoInstanceAttribute(alternative, pseudoInstance)) { + applyStyleDeclaration(element, styleWithPseudoInstance, pseudoInstance); + return; } } } + /** + * Returns {@code true} if {@code selector} carries a pseudo-class or + * attribute selector (anywhere in a compound or descendant combinator) + * whose target value equals {@code pseudoInstance}. Mirrors the legacy + * SAC walker, which handled both {@code :selected} (a pseudo-class) and + * {@code Shell[active='true']} (an attribute) through SAC's shared + * {@code AttributeCondition} interface. + */ + private static boolean matchesPseudoInstanceAttribute(Selectors.Selector selector, String pseudoInstance) { + if (selector instanceof Selectors.PseudoClass pc) { + return pseudoInstance.equals(pc.name()); + } + if (selector instanceof Selectors.AttributeSelector attr) { + return pseudoInstance.equals(attr.value()); + } + if (selector instanceof Selectors.AttributeIncludes attr) { + return pseudoInstance.equals(attr.value()); + } + if (selector instanceof Selectors.AttributeBeginHyphen attr) { + return pseudoInstance.equals(attr.value()); + } + if (selector instanceof Selectors.And and) { + return matchesPseudoInstanceAttribute(and.left(), pseudoInstance) + || matchesPseudoInstanceAttribute(and.right(), pseudoInstance); + } + if (selector instanceof Selectors.Descendant d) { + return matchesPseudoInstanceAttribute(d.descendant(), pseudoInstance) + || matchesPseudoInstanceAttribute(d.ancestor(), pseudoInstance); + } + if (selector instanceof Selectors.Child c) { + return matchesPseudoInstanceAttribute(c.child(), pseudoInstance) + || matchesPseudoInstanceAttribute(c.parent(), pseudoInstance); + } + if (selector instanceof Selectors.Adjacent a) { + return matchesPseudoInstanceAttribute(a.second(), pseudoInstance) + || matchesPseudoInstanceAttribute(a.first(), pseudoInstance); + } + return false; + } + protected String[] getStaticPseudoInstances(Element element) { if (element instanceof CSSStylableElement stylableElement) { return stylableElement.getStaticPseudoInstances(); @@ -933,18 +972,21 @@ protected Map getElementsContext() { } @Override - public boolean matches(Selector selector, Object element, String pseudoElt) { + public boolean matches(Selectors.Selector selector, Object element, String pseudoElt) { Element elt = getElement(element); if (elt == null) { return false; } - if (selector instanceof ExtendedSelector extendedSelector) { - return extendedSelector.match(elt, pseudoElt); - } else { - // TODO : selector is not batik ExtendedSelector, - // Manage this case... + int depth = 0; + for (Node n = elt; n instanceof Element; n = n.getParentNode()) { + depth++; } - return false; + Element[] hierarchy = new Element[depth]; + int idx = 0; + for (Node n = elt; n instanceof Element; n = n.getParentNode()) { + hierarchy[idx++] = (Element) n; + } + return SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0); } /*--------------- Error Handler -----------------*/ @@ -1118,14 +1160,16 @@ public String convert(Object value, Object toType, Object context) } /** - * Return instance of CSS Parser, configured with the Batik selector - * factory and the platform's class/id condition factory. + * Return instance of CSS Parser, configured with Batik's stock selector + * and condition factories. Selectors flow through {@link SacTranslator} + * before they reach engine code, so we no longer need our vendored copies + * of the SAC factory classes. */ public CSSParser makeCSSParser() { ICSSParserFactory factory = CSSParserFactory.newInstance(); CSSParser parser = factory.makeCSSParser(); - parser.setSelectorFactory(CSSSelectorFactoryImpl.INSTANCE); - parser.setConditionFactory(CONDITIONFACTORY_INSTANCE); + parser.setSelectorFactory(DefaultSelectorFactory.INSTANCE); + parser.setConditionFactory(DefaultConditionFactory.INSTANCE); return parser; } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SacTranslator.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SacTranslator.java new file mode 100644 index 00000000000..5ee9a3d5dbf --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SacTranslator.java @@ -0,0 +1,160 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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 + * + * Contributors: + * Lars Vogel - initial API and implementation + *******************************************************************************/ +package org.eclipse.e4.ui.css.core.impl.engine.selector; + +import java.util.ArrayList; +import java.util.List; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Adjacent; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.And; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeBeginHyphen; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeIncludes; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.AttributeSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Child; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ClassSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Descendant; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.ElementType; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.IdSelector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.PseudoClass; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Universal; +import org.w3c.css.sac.AttributeCondition; +import org.w3c.css.sac.CombinatorCondition; +import org.w3c.css.sac.Condition; +import org.w3c.css.sac.ConditionalSelector; +import org.w3c.css.sac.DescendantSelector; +import org.w3c.css.sac.ElementSelector; +import org.w3c.css.sac.SelectorList; +import org.w3c.css.sac.SiblingSelector; +import org.w3c.css.sac.SimpleSelector; + +/** + * Converts a SAC selector tree (as produced by the Batik parser) into the + * engine's internal {@link Selectors} AST. + * + *

+ * The translator is the single boundary between the SAC parser output and the + * rest of the engine. Once a stylesheet has been parsed, only the internal + * AST flows through {@code CSSEngine.matches}, the rule list, and + * {@link SelectorMatcher}. SAC types do not cross this boundary. + *

+ * + *

+ * Specificity is preserved exactly: the internal records compute it the same + * way the legacy SAC wrappers did (100 per id, 10 per class / attribute / + * pseudo-class, 1 per element, 0 for {@code *}). Combinators sum operands. + *

+ */ +public final class SacTranslator { + + private SacTranslator() { + // statics only + } + + /** Translate an entire {@link SelectorList} into the internal form. */ + public static Selectors.SelectorList translate(SelectorList sacList) { + List alternatives = new ArrayList<>(sacList.getLength()); + for (int i = 0; i < sacList.getLength(); i++) { + alternatives.add(translate(sacList.item(i))); + } + return new Selectors.SelectorList(alternatives); + } + + /** Translate a single SAC {@link org.w3c.css.sac.Selector}. */ + public static Selector translate(org.w3c.css.sac.Selector sac) { + return switch (sac.getSelectorType()) { + case org.w3c.css.sac.Selector.SAC_ELEMENT_NODE_SELECTOR -> translateElement((ElementSelector) sac); + case org.w3c.css.sac.Selector.SAC_PSEUDO_ELEMENT_SELECTOR -> translatePseudoElement((ElementSelector) sac); + case org.w3c.css.sac.Selector.SAC_CONDITIONAL_SELECTOR -> translateConditional((ConditionalSelector) sac); + case org.w3c.css.sac.Selector.SAC_DESCENDANT_SELECTOR -> { + DescendantSelector d = (DescendantSelector) sac; + yield new Descendant(translate(d.getAncestorSelector()), translateSimple(d.getSimpleSelector())); + } + case org.w3c.css.sac.Selector.SAC_CHILD_SELECTOR -> { + DescendantSelector c = (DescendantSelector) sac; + yield new Child(translate(c.getAncestorSelector()), translateSimple(c.getSimpleSelector())); + } + case org.w3c.css.sac.Selector.SAC_DIRECT_ADJACENT_SELECTOR -> { + SiblingSelector s = (SiblingSelector) sac; + yield new Adjacent(translate(s.getSelector()), translateSimple(s.getSiblingSelector())); + } + default -> throw new IllegalArgumentException( + "Unsupported SAC selector type: " + sac.getSelectorType() + " (" + sac + ")"); //$NON-NLS-1$ //$NON-NLS-2$ + }; + } + + private static Selector translateSimple(SimpleSelector simple) { + return translate(simple); + } + + private static Selector translateElement(ElementSelector sel) { + String name = sel.getLocalName(); + return name == null ? new Universal() : new ElementType(name); + } + + private static Selector translatePseudoElement(ElementSelector sel) { + // Pseudo-element form (::first-line). The engine has never matched + // these; treat as the element it appears on (if any) or universal. + // In practice the parser only emits this when a stylesheet uses :: , + // which the supported subset does not. + String name = sel.getLocalName(); + return name == null ? new Universal() : new PseudoClass(name); + } + + private static Selector translateConditional(ConditionalSelector sel) { + Selector left = translate(sel.getSimpleSelector()); + Selector right = translateCondition(sel.getCondition()); + if (left instanceof Universal) { + return right; + } + return new And(left, right); + } + + private static Selector translateCondition(Condition condition) { + return switch (condition.getConditionType()) { + case Condition.SAC_CLASS_CONDITION -> new ClassSelector(((AttributeCondition) condition).getValue()); + case Condition.SAC_ID_CONDITION -> new IdSelector(((AttributeCondition) condition).getValue()); + case Condition.SAC_PSEUDO_CLASS_CONDITION -> new PseudoClass(((AttributeCondition) condition).getValue()); + case Condition.SAC_LANG_CONDITION -> { + // Modeled as a presence-form attribute selector keyed on lang; + // nothing in the supported subset uses :lang(), but the parser + // can still emit it. + AttributeCondition lang = (AttributeCondition) condition; + yield new AttributeSelector("lang", lang.getValue()); //$NON-NLS-1$ + } + case Condition.SAC_ATTRIBUTE_CONDITION -> { + AttributeCondition attr = (AttributeCondition) condition; + // Batik's stock Parser always calls createAttributeCondition with + // specified=false, regardless of whether the source was [attr] or + // [attr='value']. Distinguish the two by whether a value was + // supplied: null means the presence form [attr]. + yield new AttributeSelector(attr.getLocalName(), attr.getValue()); + } + case Condition.SAC_ONE_OF_ATTRIBUTE_CONDITION -> { + AttributeCondition attr = (AttributeCondition) condition; + yield new AttributeIncludes(attr.getLocalName(), attr.getValue()); + } + case Condition.SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION -> { + AttributeCondition attr = (AttributeCondition) condition; + yield new AttributeBeginHyphen(attr.getLocalName(), attr.getValue()); + } + case Condition.SAC_AND_CONDITION -> { + CombinatorCondition combo = (CombinatorCondition) condition; + yield new And(translateCondition(combo.getFirstCondition()), + translateCondition(combo.getSecondCondition())); + } + default -> throw new IllegalArgumentException( + "Unsupported SAC condition type: " + condition.getConditionType() + " (" + condition + ")"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + }; + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java index 602582f39a5..82e97624dd3 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/SelectorMatcher.java @@ -61,6 +61,14 @@ private SelectorMatcher() { * the given pseudo state. */ public static boolean matches(Selector selector, Element element, String pseudoElement) { + return matches(selector, element, pseudoElement, null, 0); + } + + /** + * @return {@code true} if {@code selector} matches {@code element} for + * the given pseudo state, using a pre-computed ancestor hierarchy array if available. + */ + public static boolean matches(Selector selector, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { if (element == null) { return false; } @@ -89,19 +97,20 @@ public static boolean matches(Selector selector, Element element, String pseudoE return matchesPseudoClass(pc, element, pseudoElement); } if (selector instanceof And and) { - return matches(and.left(), element, pseudoElement) && matches(and.right(), element, pseudoElement); + return matches(and.left(), element, pseudoElement, hierarchy, hierarchyIndex) + && matches(and.right(), element, pseudoElement, hierarchy, hierarchyIndex); } if (selector instanceof Descendant d) { - return matchesDescendant(d, element, pseudoElement); + return matchesDescendant(d, element, pseudoElement, hierarchy, hierarchyIndex); } if (selector instanceof Child c) { - return matchesChild(c, element, pseudoElement); + return matchesChild(c, element, pseudoElement, hierarchy, hierarchyIndex); } if (selector instanceof Adjacent a) { - return matchesAdjacent(a, element, pseudoElement); + return matchesAdjacent(a, element, pseudoElement, hierarchy, hierarchyIndex); } if (selector instanceof SelectorList list) { - return matchesAny(list, element, pseudoElement); + return matchesAny(list, element, pseudoElement, hierarchy, hierarchyIndex); } throw new IllegalStateException("Unknown selector kind: " + selector.getClass()); //$NON-NLS-1$ } @@ -138,16 +147,15 @@ private static boolean matchesId(IdSelector id, Element element) { private static boolean matchesAttribute(AttributeSelector attr, Element element) { String name = attr.name(); + if (!element.hasAttribute(name)) { + return false; + } String required = attr.value(); if (required == null) { // presence form: [attr] - return element.hasAttribute(name); - } - String actual = element.getAttribute(name); - if (actual == null) { - return required.isEmpty(); + return true; } - return required.equals(actual); + return required.equals(element.getAttribute(name)); } private static boolean matchesAttributeIncludes(AttributeIncludes inc, Element element) { @@ -219,42 +227,59 @@ private static boolean matchesPseudoClass(PseudoClass pseudo, Element element, S return true; } - private static boolean matchesDescendant(Descendant d, Element element, String pseudoElement) { - if (!matches(d.descendant(), element, pseudoElement)) { + private static boolean matchesDescendant(Descendant d, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (!matches(d.descendant(), element, pseudoElement, hierarchy, hierarchyIndex)) { return false; } - Node parent = element.getParentNode(); - while (parent instanceof Element parentElement) { - if (matches(d.ancestor(), parentElement, null)) { - return true; + if (hierarchy != null) { + for (int i = hierarchyIndex + 1; i < hierarchy.length; i++) { + if (matches(d.ancestor(), hierarchy[i], null, hierarchy, i)) { + return true; + } } - parent = parentElement.getParentNode(); + return false; + } else { + Node parent = element.getParentNode(); + while (parent instanceof Element parentElement) { + if (matches(d.ancestor(), parentElement, null, null, 0)) { + return true; + } + parent = parentElement.getParentNode(); + } + return false; } - return false; } - private static boolean matchesChild(Child c, Element element, String pseudoElement) { - if (!matches(c.child(), element, pseudoElement)) { + private static boolean matchesChild(Child c, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (!matches(c.child(), element, pseudoElement, hierarchy, hierarchyIndex)) { + return false; + } + if (hierarchy != null) { + int parentIdx = hierarchyIndex + 1; + if (parentIdx < hierarchy.length) { + return matches(c.parent(), hierarchy[parentIdx], null, hierarchy, parentIdx); + } return false; + } else { + Node parent = element.getParentNode(); + return parent instanceof Element parentElement && matches(c.parent(), parentElement, null, null, 0); } - Node parent = element.getParentNode(); - return parent instanceof Element parentElement && matches(c.parent(), parentElement, null); } - private static boolean matchesAdjacent(Adjacent a, Element element, String pseudoElement) { - if (!matches(a.second(), element, pseudoElement)) { + private static boolean matchesAdjacent(Adjacent a, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { + if (!matches(a.second(), element, pseudoElement, hierarchy, hierarchyIndex)) { return false; } Node previous = element.getPreviousSibling(); while (previous != null && !(previous instanceof Element)) { previous = previous.getPreviousSibling(); } - return previous instanceof Element previousElement && matches(a.first(), previousElement, null); + return previous instanceof Element previousElement && matches(a.first(), previousElement, null, null, 0); } - private static boolean matchesAny(SelectorList list, Element element, String pseudoElement) { + private static boolean matchesAny(SelectorList list, Element element, String pseudoElement, Element[] hierarchy, int hierarchyIndex) { for (Selector alternative : list.alternatives()) { - if (matches(alternative, element, pseudoElement)) { + if (matches(alternative, element, pseudoElement, hierarchy, hierarchyIndex)) { return true; } } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java index 552163a3a19..ebca60b165b 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/selector/Selectors.java @@ -63,7 +63,12 @@ public int specificity() { @Override public String text() { - return "*"; + return "*"; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -78,6 +83,11 @@ public int specificity() { public String text() { return localName; } + + @Override + public String toString() { + return text(); + } } /** {@code .foo} — matches by CSS class. */ @@ -89,7 +99,12 @@ public int specificity() { @Override public String text() { - return "." + className; + return "." + className; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -102,7 +117,12 @@ public int specificity() { @Override public String text() { - return "#" + id; + return "#" + id; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -118,7 +138,12 @@ public int specificity() { @Override public String text() { - return value == null ? "[" + name + "]" : "[" + name + "='" + value + "']"; + return value == null ? "[" + name + "]" : "[" + name + "='" + value + "']"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ + } + + @Override + public String toString() { + return text(); } } @@ -131,7 +156,12 @@ public int specificity() { @Override public String text() { - return "[" + name + "~='" + value + "']"; + return "[" + name + "~='" + value + "']"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + public String toString() { + return text(); } } @@ -144,7 +174,12 @@ public int specificity() { @Override public String text() { - return "[" + name + "|='" + value + "']"; + return "[" + name + "|='" + value + "']"; //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ + } + + @Override + public String toString() { + return text(); } } @@ -160,7 +195,12 @@ public int specificity() { @Override public String text() { - return ":" + name; + return ":" + name; //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -179,6 +219,11 @@ public int specificity() { public String text() { return left.text() + right.text(); } + + @Override + public String toString() { + return text(); + } } /** {@code ancestor descendant} — descendant combinator. */ @@ -190,7 +235,12 @@ public int specificity() { @Override public String text() { - return ancestor.text() + " " + descendant.text(); + return ancestor.text() + " " + descendant.text(); //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -203,7 +253,12 @@ public int specificity() { @Override public String text() { - return parent.text() + " > " + child.text(); + return parent.text() + " > " + child.text(); //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -216,7 +271,12 @@ public int specificity() { @Override public String text() { - return first.text() + " + " + second.text(); + return first.text() + " + " + second.text(); //$NON-NLS-1$ + } + + @Override + public String toString() { + return text(); } } @@ -252,6 +312,16 @@ public List alternatives() { return alternatives; } + /** Number of alternatives in the list. SAC-style accessor for callers iterating the list. */ + public int getLength() { + return alternatives.size(); + } + + /** {@code i}-th alternative. SAC-style accessor for callers iterating the list. */ + public Selector item(int i) { + return alternatives.get(i); + } + @Override public int specificity() { return specificity; diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java deleted file mode 100644 index fabffbad828..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractAttributeCondition.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Objects; -import org.w3c.css.sac.AttributeCondition; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.AttributeCondition} interface. - */ -public abstract class AbstractAttributeCondition implements AttributeCondition, - ExtendedCondition { - - /** - * The attribute value. - */ - protected String value; - - /** - * Creates a new AbstractAttributeCondition object. - */ - protected AbstractAttributeCondition(String value) { - this.value = value; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractAttributeCondition c = (AbstractAttributeCondition) obj; - return c.value.equals(value); - } - - /** - * equal objects should have equal hashCodes. - * - * @return hashCode of this AbstractAttributeCondition - */ - @Override - public int hashCode() { - return Objects.hashCode(value); - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return 1 << 8; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getValue()}. - */ - @Override - public String getValue() { - return value; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java deleted file mode 100644 index 0de6c81b86c..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractCombinatorCondition.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.CombinatorCondition} interface. - */ -public abstract class AbstractCombinatorCondition implements - CombinatorCondition, ExtendedCondition { - - /** - * The first condition. - */ - protected Condition firstCondition; - - /** - * The second condition. - */ - protected Condition secondCondition; - - /** - * Creates a new CombinatorCondition object. - */ - protected AbstractCombinatorCondition(Condition c1, Condition c2) { - firstCondition = c1; - secondCondition = c2; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractCombinatorCondition c = (AbstractCombinatorCondition) obj; - return (c.firstCondition.equals(firstCondition) && c.secondCondition - .equals(secondCondition)); - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return ((ExtendedCondition) getFirstCondition()).getSpecificity() - + ((ExtendedCondition) getSecondCondition()).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.CombinatorCondition#getFirstCondition()}. - */ - @Override - public Condition getFirstCondition() { - return firstCondition; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.CombinatorCondition#getSecondCondition()}. - */ - @Override - public Condition getSecondCondition() { - return secondCondition; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java deleted file mode 100644 index f183af1ccae..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractDescendantSelector.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.DescendantSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.DescendantSelector} interface. - */ -public abstract class AbstractDescendantSelector - implements DescendantSelector, - ExtendedSelector { - - /** - * The ancestor selector. - */ - protected Selector ancestorSelector; - - /** - * The simple selector. - */ - protected SimpleSelector simpleSelector; - - /** - * Creates a new DescendantSelector object. - */ - protected AbstractDescendantSelector(Selector ancestor, - SimpleSelector simple) { - ancestorSelector = ancestor; - simpleSelector = simple; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractDescendantSelector s = (AbstractDescendantSelector)obj; - return s.simpleSelector.equals(simpleSelector); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return ((ExtendedSelector)ancestorSelector).getSpecificity() + - ((ExtendedSelector)simpleSelector).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.DescendantSelector#getAncestorSelector()}. - */ - @Override - public Selector getAncestorSelector() { - return ancestorSelector; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.DescendantSelector#getSimpleSelector()}. - */ - @Override - public SimpleSelector getSimpleSelector() { - return simpleSelector; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java deleted file mode 100644 index 529b86a0fbb..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractElementSelector.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.ElementSelector; - -/** - * This class provides an abstract implementation of the ElementSelector - * interface. - */ -public abstract class AbstractElementSelector implements ElementSelector, ExtendedSelector { - - /** - * The namespace URI. - */ - protected String namespaceURI; - - /** - * The local name. - */ - protected String localName; - - /** - * Creates a new ElementSelector object. - */ - protected AbstractElementSelector(String uri, String name) { - namespaceURI = uri; - localName = name; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractElementSelector s = (AbstractElementSelector)obj; - return (s.namespaceURI.equals(namespaceURI) && s.localName.equals(localName)); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ElementSelector#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ElementSelector#getLocalName()}. - */ - @Override - public String getLocalName() { - return localName; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java deleted file mode 100644 index bb061dd13e0..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/AbstractSiblingSelector.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SiblingSelector; -import org.w3c.css.sac.SimpleSelector; - -/** - * This class provides an abstract implementation of the {@link - * org.w3c.css.sac.SiblingSelector} interface. - */ -public abstract class AbstractSiblingSelector implements SiblingSelector, - ExtendedSelector { - - /** - * The node type. - */ - protected short nodeType; - - /** - * The selector. - */ - protected Selector selector; - - /** - * The simple selector. - */ - protected SimpleSelector simpleSelector; - - /** - * Creates a new SiblingSelector object. - */ - protected AbstractSiblingSelector(short type, Selector sel, - SimpleSelector simple) { - nodeType = type; - selector = sel; - simpleSelector = simple; - } - - /** - * Returns the node type. - */ - @Override - public short getNodeType() { - return nodeType; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - AbstractSiblingSelector s = (AbstractSiblingSelector) obj; - return s.simpleSelector.equals(simpleSelector); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return ((ExtendedSelector) selector).getSpecificity() - + ((ExtendedSelector) simpleSelector).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SiblingSelector#getSelector()}. - */ - @Override - public Selector getSelector() { - return selector; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SiblingSelector#getSiblingSelector()}. - */ - @Override - public SimpleSelector getSiblingSelector() { - return simpleSelector; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java deleted file mode 100644 index b9d528d05ad..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAndConditionImpl.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Condition; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.CombinatorCondition} interface. - */ -public class CSSAndConditionImpl extends AbstractCombinatorCondition { - /** - * Creates a new CombinatorCondition object. - */ - public CSSAndConditionImpl(Condition c1, Condition c2) { - super(c1, c2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_AND_CONDITION; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return ((ExtendedCondition)getFirstCondition()).match(e, pseudoE) && - ((ExtendedCondition)getSecondCondition()).match(e, pseudoE); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedCondition)getFirstCondition()).fillAttributeSet(attrSet); - ((ExtendedCondition)getSecondCondition()).fillAttributeSet(attrSet); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return String.valueOf( getFirstCondition() ) + getSecondCondition(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java deleted file mode 100644 index 3467d068938..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSAttributeConditionImpl.java +++ /dev/null @@ -1,153 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSAttributeConditionImpl extends AbstractAttributeCondition { - /** - * The attribute's local name. - */ - protected String localName; - - /** - * The attribute's namespace URI. - */ - protected String namespaceURI; - - /** - * Whether this condition applies to specified attributes. - */ - protected boolean specified; - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSAttributeConditionImpl(String localName, String namespaceURI, - boolean specified, String value) { - super(value); - this.localName = localName; - this.namespaceURI = namespaceURI; - this.specified = specified; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) { - return false; - } - CSSAttributeConditionImpl c = (CSSAttributeConditionImpl) obj; - return (c.namespaceURI.equals(namespaceURI) - && c.localName.equals(localName) && c.specified == specified); - } - - /** - * equal objects should have equal hashCodes. - * - * @return hashCode of this CSSAttributeCondition - */ - @Override - public int hashCode() { - return namespaceURI.hashCode() ^ localName.hashCode() - ^ Boolean.hashCode(specified); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_ATTRIBUTE_CONDITION; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getLocalName()}. - */ - @Override - public String getLocalName() { - return localName; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getSpecified()}. - */ - @Override - public boolean getSpecified() { - return specified; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - if (!e.hasAttribute(getLocalName())) { - return false; - } - String val = getValue(); - if (val == null) { - return true; - } - return e.getAttribute(getLocalName()).equals(val); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - attrSet.add(localName); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - if (value == null) { - return '[' + localName + ']'; - } - return '[' + localName + "=\"" + value + "\"]"; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java deleted file mode 100644 index 12064bb8e47..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSBeginHyphenAttributeConditionImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSBeginHyphenAttributeConditionImpl extends - CSSAttributeConditionImpl { - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSBeginHyphenAttributeConditionImpl(String localName, - String namespaceURI, boolean specified, String value) { - super(localName, namespaceURI, specified, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_BEGIN_HYPHEN_ATTRIBUTE_CONDITION; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return e.getAttribute(getLocalName()).startsWith(getValue()); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return '[' + getLocalName() + "|=\"" + getValue() + "\"]"; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java deleted file mode 100644 index 450031025f2..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSChildSelectorImpl.java +++ /dev/null @@ -1,102 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.DescendantSelector} interface. - */ -public class CSSChildSelectorImpl extends AbstractDescendantSelector { - - /** - * Creates a new CSSChildSelector object. - */ - public CSSChildSelectorImpl(Selector ancestor, SimpleSelector simple) { - super(ancestor, simple); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_CHILD_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, Node[] hierarchy, int parentIndex, String pseudoE) { - if (hierarchy == null || parentIndex >= hierarchy.length) { - return false; - } - - Node n = hierarchy[parentIndex]; - if (n != null && n.getNodeType() == Node.ELEMENT_NODE) { - return ((ExtendedSelector) getAncestorSelector()).match((Element) n, hierarchy, parentIndex + 1, null) - && ((ExtendedSelector) getSimpleSelector()).match(e, hierarchy, parentIndex + 1, pseudoE); - } - return false; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - Node n = e.getParentNode(); - if (n != null && n.getNodeType() == Node.ELEMENT_NODE) { - return ((ExtendedSelector) getAncestorSelector()).match((Element) n, - null) - && ((ExtendedSelector) getSimpleSelector()).match(e, pseudoE); - } - return false; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getAncestorSelector()).fillAttributeSet(attrSet); - ((ExtendedSelector)getSimpleSelector()).fillAttributeSet(attrSet); - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - SimpleSelector s = getSimpleSelector(); - if (s.getSelectorType() == SAC_PSEUDO_ELEMENT_SELECTOR) { - return String.valueOf( getAncestorSelector() ) + s; - } - return getAncestorSelector() + " > " + s; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java deleted file mode 100644 index 521c7743af1..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSClassConditionImpl.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSClassConditionImpl extends CSSAttributeConditionImpl { - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSClassConditionImpl(String localName, String namespaceURI, - String value) { - super(localName, namespaceURI, true, value); - } - - @Override - public boolean match(Element e, String pseudoE) { - String attr = null; - if ((e instanceof CSSStylableElement)) { - attr = ((CSSStylableElement) e).getCSSClass(); - } else { - attr = e.getAttribute("class"); - } - if (attr == null || attr.length() < 1) { - return false; - } - String val = getValue(); - int attrLen = attr.length(); - int valLen = val.length(); - for (int i = attr.indexOf(val); i != -1; i = attr.indexOf(val, i - + valLen)) { - if ((i == 0 || Character.isSpaceChar(attr.charAt(i - 1))) - && (i + valLen == attrLen || Character.isSpaceChar(attr - .charAt(i + valLen)))) { - return true; - } - } - - return false; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java deleted file mode 100644 index 24676814228..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionFactoryImpl.java +++ /dev/null @@ -1,213 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.AttributeCondition; -import org.w3c.css.sac.CSSException; -import org.w3c.css.sac.CombinatorCondition; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionFactory; -import org.w3c.css.sac.ContentCondition; -import org.w3c.css.sac.LangCondition; -import org.w3c.css.sac.NegativeCondition; -import org.w3c.css.sac.PositionalCondition; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.ConditionFactory} interface. - */ -public class CSSConditionFactoryImpl implements ConditionFactory { - - private static final String NOT_IMPLEMENTED_IN_CSS2 = "Not implemented in CSS2"; //$NON-NLS-1$ - - /** - * The class attribute namespace URI. - */ - protected String classNamespaceURI; - - /** - * The class attribute local name. - */ - protected String classLocalName; - - /** - * The id attribute namespace URI. - */ - protected String idNamespaceURI; - - /** - * The id attribute local name. - */ - protected String idLocalName; - - /** - * Creates a new condition factory. - */ - public CSSConditionFactoryImpl(String cns, String cln, String idns, - String idln) { - classNamespaceURI = cns; - classLocalName = cln; - idNamespaceURI = idns; - idLocalName = idln; - } - - /** - * SAC: Implements {@link - * ConditionFactory#createAndCondition(Condition,Condition)}. - */ - @Override - public CombinatorCondition createAndCondition(Condition first, - Condition second) throws CSSException { - return new CSSAndConditionImpl(first, second); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createOrCondition(Condition,Condition)}. - */ - @Override - public CombinatorCondition createOrCondition(Condition first, - Condition second) throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createNegativeCondition(Condition)}. - */ - @Override - public NegativeCondition createNegativeCondition(Condition condition) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createPositionalCondition(int,boolean,boolean)}. - */ - @Override - public PositionalCondition createPositionalCondition(int position, - boolean typeNode, boolean type) throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createAttributeCondition(String,String,boolean,String)}. - */ - @Override - public AttributeCondition createAttributeCondition(String localName, - String namespaceURI, boolean specified, String value) - throws CSSException { - return new CSSAttributeConditionImpl(localName, namespaceURI, specified, - value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createIdCondition(String)}. - */ - @Override - public AttributeCondition createIdCondition(String value) - throws CSSException { - return new CSSIdConditionImpl(idNamespaceURI, idLocalName, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createLangCondition(String)}. - */ - @Override - public LangCondition createLangCondition(String lang) throws CSSException { - return new CSSLangConditionImpl(lang); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createOneOfAttributeCondition(String,String,boolean,String)}. - */ - @Override - public AttributeCondition createOneOfAttributeCondition(String localName, - String nsURI, boolean specified, String value) throws CSSException { - return new CSSOneOfAttributeConditionImpl(localName, nsURI, specified, - value); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createBeginHyphenAttributeCondition(String,String,boolean,String)}. - */ - @Override - public AttributeCondition createBeginHyphenAttributeCondition( - String localName, String namespaceURI, boolean specified, - String value) throws CSSException { - return new CSSBeginHyphenAttributeConditionImpl(localName, - namespaceURI, specified, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createClassCondition(String,String)}. - */ - @Override - public AttributeCondition createClassCondition(String namespaceURI, - String value) throws CSSException { - return new CSSClassConditionImpl(classLocalName, classNamespaceURI, value); - } - - /** - * SAC: Implements {@link - * ConditionFactory#createPseudoClassCondition(String,String)}. - */ - @Override - public AttributeCondition createPseudoClassCondition(String namespaceURI, - String value) throws CSSException { - return new CSSPseudoClassConditionImpl(namespaceURI, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createOnlyChildCondition()}. - */ - @Override - public Condition createOnlyChildCondition() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createOnlyTypeCondition()}. - */ - @Override - public Condition createOnlyTypeCondition() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionFactory#createContentCondition(String)}. - */ - @Override - public ContentCondition createContentCondition(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java deleted file mode 100644 index c4cc09ad9c1..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSConditionalSelectorImpl.java +++ /dev/null @@ -1,135 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.ConditionalSelector} interface. - */ -public class CSSConditionalSelectorImpl implements ConditionalSelector, ExtendedSelector { - - /** - * The simple selector. - */ - protected SimpleSelector simpleSelector; - - /** - * The condition. - */ - protected Condition condition; - - /** - * Creates a new ConditionalSelector object. - */ - public CSSConditionalSelectorImpl(SimpleSelector s, Condition c) { - simpleSelector = s; - condition = c; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - CSSConditionalSelectorImpl s = (CSSConditionalSelectorImpl)obj; - return (s.simpleSelector.equals(simpleSelector) && - s.condition.equals(condition)); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_CONDITIONAL_SELECTOR; - } - - @Override - public boolean match(Element e, Node[] hierarchy, int parentIndex, String pseudoE) { - return ((ExtendedSelector)getSimpleSelector()).match(e, hierarchy, parentIndex, pseudoE) && - ((ExtendedCondition)getCondition()).match(e, pseudoE); - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return ((ExtendedSelector)getSimpleSelector()).match(e, pseudoE) && - ((ExtendedCondition)getCondition()).match(e, pseudoE); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getSimpleSelector()).fillAttributeSet(attrSet); - ((ExtendedCondition)getCondition()).fillAttributeSet(attrSet); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return ((ExtendedSelector)getSimpleSelector()).getSpecificity() + - ((ExtendedCondition)getCondition()).getSpecificity(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionalSelector#getSimpleSelector()}. - */ - @Override - public SimpleSelector getSimpleSelector() { - return simpleSelector; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.ConditionalSelector#getCondition()}. - */ - @Override - public Condition getCondition() { - return condition; - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return String.valueOf( simpleSelector ) + condition; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java deleted file mode 100644 index 44f3568dfc3..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDescendantSelectorImpl.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation for the - * {@link org.w3c.css.sac.DescendantSelector} interface. - */ -public class CSSDescendantSelectorImpl extends AbstractDescendantSelector { - - /** - * Creates a new CSSDescendantSelector object. - */ - public CSSDescendantSelectorImpl(Selector ancestor, SimpleSelector simple) { - super(ancestor, simple); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_DESCENDANT_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, Node[] hierarchy, int parentIndex, String pseudoE) { - ExtendedSelector p = (ExtendedSelector) getAncestorSelector(); - if (!((ExtendedSelector) getSimpleSelector()).match(e, hierarchy, parentIndex, pseudoE)) { - return false; - } - - if (hierarchy == null) { - return false; - } - - Node n; - int length = hierarchy.length; - for (int i = parentIndex; i < length; i++) { - n = hierarchy[i]; - if (n != null && n.getNodeType() == Node.ELEMENT_NODE - && p.match((Element) n, hierarchy, i + 1, null)) { - return true; - } - } - return false; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - ExtendedSelector p = (ExtendedSelector) getAncestorSelector(); - if (!((ExtendedSelector) getSimpleSelector()).match(e, pseudoE)) { - return false; - } - for (Node n = e.getParentNode(); n != null; n = n.getParentNode()) { - if (n.getNodeType() == Node.ELEMENT_NODE && p.match((Element) n, null)) { - return true; - } - } - return false; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getSimpleSelector()).fillAttributeSet(attrSet); - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return getAncestorSelector() + " " + getSimpleSelector(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java deleted file mode 100644 index 73eea3eb684..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDirectAdjacentSelectorImpl.java +++ /dev/null @@ -1,106 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SimpleSelector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This class provides an implementation for the - * {@link org.w3c.css.sac.DescendantSelector} interface. - */ -public class CSSDirectAdjacentSelectorImpl extends AbstractSiblingSelector { - - /** - * Creates a new CSSDirectAdjacentSelector object. - */ - public CSSDirectAdjacentSelectorImpl(short type, Selector parent, SimpleSelector simple) { - super(type, parent, simple); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_DIRECT_ADJACENT_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, Node[] hiearchy, int parentIndex, String pseudoE) { - Node n = e; - if (!((ExtendedSelector) getSiblingSelector()).match(e, hiearchy, parentIndex, pseudoE)) { - return false; - } - - while ((n = n.getPreviousSibling()) != null && n.getNodeType() != Node.ELEMENT_NODE) { - } - - if (n == null) { - return false; - } - - return ((ExtendedSelector) getSelector()).match((Element) n, hiearchy, parentIndex, null); - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - Node n = e; - if (!((ExtendedSelector)getSiblingSelector()).match(e, pseudoE)) { - return false; - } - while ((n = n.getPreviousSibling()) != null && n.getNodeType() != Node.ELEMENT_NODE) { - } - - if (n == null) { - return false; - } - - return ((ExtendedSelector)getSelector()).match((Element)n, null); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - ((ExtendedSelector)getSelector()).fillAttributeSet(attrSet); - ((ExtendedSelector)getSiblingSelector()).fillAttributeSet(attrSet); - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return getSelector() + " + " + getSiblingSelector(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java index 150c95c4b7a..9456b6a94e2 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSDocumentHandlerImpl.java @@ -25,6 +25,7 @@ import org.eclipse.e4.ui.css.core.impl.dom.CSSUnknownRuleImpl; import org.eclipse.e4.ui.css.core.impl.dom.CSSValueFactory; import org.eclipse.e4.ui.css.core.impl.dom.MediaListImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.SacTranslator; import org.eclipse.e4.ui.css.core.sac.ExtendedDocumentHandler; import org.w3c.css.sac.CSSException; import org.w3c.css.sac.InputSource; @@ -165,9 +166,10 @@ public void endFontFace() throws CSSException { @Override public void startSelector(SelectorList selectors) throws CSSException { - // Create the style rule and add it to the rule list + // Translate the SAC selector list into the engine's internal AST at + // this boundary so nothing downstream needs to touch SAC types. CSSStyleRuleImpl rule = new CSSStyleRuleImpl(parentStyleSheet, null, - selectors); + SacTranslator.translate(selectors)); if (!getNodeStack().empty()) { ((CSSRuleListImpl) getNodeStack().peek()).add(rule); } diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java deleted file mode 100644 index f24e1aeea7f..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSElementSelectorImpl.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.dom.Element; - -/** - * This class implements the {@link org.w3c.css.sac.ElementSelector} interface. - */ -public class CSSElementSelectorImpl extends AbstractElementSelector { - - /** - * Creates a new ElementSelector object. - */ - public CSSElementSelectorImpl(String uri, String name) { - super(uri, name); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_ELEMENT_NODE_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String name = getLocalName(); - if (name == null) { - if (namespaceURI != null) { - return namespaceURI.equals(e.getNamespaceURI()); - } else { - return true; - } - } - String eName; - if (e.getPrefix() == null) { - eName = e.getNodeName(); - } else { - eName = e.getLocalName(); - } - // According to CSS 2 section 5.1 element - // names in selectors are case-sensitive for XML. - if (eName.equals(name)) { - if (namespaceURI != null) { - return namespaceURI.equals(e.getNamespaceURI()); - } else { - return true; - } - } - return false; - // For HTML - // return eName.equalsIgnoreCase(name); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return (getLocalName() == null) ? 0 : 1; - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - String name = getLocalName(); - if (name == null) { - return "*"; - } - return name; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java deleted file mode 100644 index eec162ea03d..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSIdConditionImpl.java +++ /dev/null @@ -1,129 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSIdConditionImpl extends AbstractAttributeCondition { - - /** - * The id attribute namespace URI. - */ - protected String namespaceURI; - - /** - * The id attribute local name. - */ - protected String localName; - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSIdConditionImpl(String ns, String ln, String value) { - super(value); - namespaceURI = ns; - localName = ln; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_ID_CONDITION; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getLocalName()}. - */ - @Override - public String getLocalName() { - return localName; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getSpecified()}. - */ - @Override - public boolean getSpecified() { - return true; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String id = null; - if (e instanceof CSSStylableElement) { - id = ((CSSStylableElement) e).getCSSId(); - } else { - id = e.getAttribute("id"); - } - if (id == null) { - return false; - } - return id.equals(getValue()); - // return super.match(e, pseudoE); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - attrSet.add(localName); - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return 1 << 16; - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return '#' + getValue(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java deleted file mode 100644 index 4789469f7fa..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSLangConditionImpl.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.LangCondition; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.LangCondition} interface. - */ -public class CSSLangConditionImpl implements LangCondition, ExtendedCondition { - /** - * The language. - */ - protected String lang; - - /** - * The language with a hyphen suffixed. - */ - protected String langHyphen; - - /** - * Creates a new LangCondition object. - */ - public CSSLangConditionImpl(String lang) { - this.lang = lang.toLowerCase(); - this.langHyphen = lang + '-'; - } - - /** - * Indicates whether some other object is "equal to" this one. - * @param obj the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (obj == null || (obj.getClass() != getClass())) { - return false; - } - CSSLangConditionImpl c = (CSSLangConditionImpl)obj; - return c.lang.equals(lang); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_LANG_CONDITION; - } - - /** - * SAC: Implements {@link org.w3c.css.sac.LangCondition#getLang()}. - */ - @Override - public String getLang() { - return lang; - } - - /** - * Returns the specificity of this condition. - */ - @Override - public int getSpecificity() { - return 1 << 8; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String s = e.getAttribute("lang").toLowerCase(); - if (s.equals(lang) || s.startsWith(langHyphen)) { - return true; - } - // s = e.getAttributeNS(XMLConstants.XML_NAMESPACE_URI, - // XMLConstants.XML_LANG_ATTRIBUTE).toLowerCase(); - return s.equals(lang) || s.startsWith(langHyphen); - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - attrSet.add("lang"); - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return ":lang(" + lang + ')'; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java deleted file mode 100644 index fb4ebbd0428..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSOneOfAttributeConditionImpl.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/******************************************************************************* - * Contributors: - * This class was copied from org.apache.batik.css.engine.sac - * Apache Batik project - initial API and implementation - * Alain Le Guennec - Bug 458334 - *******************************************************************************/ -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.StringTokenizer; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSOneOfAttributeConditionImpl extends CSSAttributeConditionImpl { - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSOneOfAttributeConditionImpl(String localName, - String namespaceURI, boolean specified, String value) { - super(localName, namespaceURI, specified, value); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_ONE_OF_ATTRIBUTE_CONDITION; - } - - /** - * Tests whether this condition matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - String attr = e.getAttribute(getLocalName()); - String val = getValue(); - for (StringTokenizer tok = new StringTokenizer(attr); tok.hasMoreElements();) { - String candidate = tok.nextToken(); - if (val.equals(candidate)) { - return true; - } - } - return false; - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return "[" + getLocalName() + "~=\"" + getValue() + "\"]"; - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java deleted file mode 100644 index 1c459d6637b..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoClassConditionImpl.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.eclipse.e4.ui.css.core.dom.CSSStylableElement; -import org.w3c.dom.Element; - -/** - * This class provides an implementation of the - * {@link org.w3c.css.sac.AttributeCondition} interface. - */ -public class CSSPseudoClassConditionImpl extends AbstractAttributeCondition { - /** - * The namespaceURI. - */ - protected String namespaceURI; - - /** - * Creates a new CSSAttributeCondition object. - */ - public CSSPseudoClassConditionImpl(String namespaceURI, String value) { - super(value); - this.namespaceURI = namespaceURI; - } - - /** - * Indicates whether some other object is "equal to" this one. - * - * @param obj - * the reference object with which to compare. - */ - @Override - public boolean equals(Object obj) { - if (!super.equals(obj)) { - return false; - } - CSSPseudoClassConditionImpl c = (CSSPseudoClassConditionImpl) obj; - return c.namespaceURI.equals(namespaceURI); - } - - /** - * equal objects should have equal hashCodes. - * - * @return hashCode of this CSSPseudoClassCondition - */ - @Override - public int hashCode() { - return namespaceURI.hashCode(); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Condition#getConditionType()}. - */ - @Override - public short getConditionType() { - return SAC_PSEUDO_CLASS_CONDITION; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getNamespaceURI()}. - */ - @Override - public String getNamespaceURI() { - return namespaceURI; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getLocalName()}. - */ - @Override - public String getLocalName() { - return null; - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.AttributeCondition#getSpecified()}. - */ - @Override - public boolean getSpecified() { - return false; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - if (pseudoE != null && !pseudoE.equals(getValue())) { - // pseudo instance is filled, it is not valid. - return false; - } - if (!(e instanceof CSSStylableElement element)) { - return false; - } - boolean isPseudoInstanceOf = element.isPseudoInstanceOf(getValue()); - if (!isPseudoInstanceOf) { - return false; - } - if (pseudoE == null) { - // pseudo element is not filled. - // test if this CSSPseudoClassCondition is NOT a static pseudo - // instance - return (!element.isStaticPseudoInstance(getValue())); - } - return true; - } - - /** - * Fills the given set with the attribute names found in this selector. - */ - @Override - public void fillAttributeSet(Set attrSet) { - } - - /** - * Returns a text representation of this object. - */ - @Override - public String toString() { - return ":" + getValue(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java deleted file mode 100644 index 5353e2c4885..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSPseudoElementSelectorImpl.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.dom.Element; - -/** - * This class implements the {@link org.w3c.css.sac.ElementSelector} interface. - */ -public class CSSPseudoElementSelectorImpl extends AbstractElementSelector { - - /** - * Creates a new CSSPseudoElementSelector object. - */ - public CSSPseudoElementSelectorImpl(String uri, String name) { - super(uri, name); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.Selector#getSelectorType()}. - */ - @Override - public short getSelectorType() { - return SAC_PSEUDO_ELEMENT_SELECTOR; - } - - /** - * Tests whether this selector matches the given element. - */ - @Override - public boolean match(Element e, String pseudoE) { - return getLocalName().equalsIgnoreCase(pseudoE); - } - - /** - * Returns the specificity of this selector. - */ - @Override - public int getSpecificity() { - return 0; - } - - /** - * Returns a representation of the selector. - */ - @Override - public String toString() { - return ":" + getLocalName(); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java deleted file mode 100644 index 00370115317..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/CSSSelectorFactoryImpl.java +++ /dev/null @@ -1,182 +0,0 @@ -/* - - Copyright 2002, 2014 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import org.w3c.css.sac.CSSException; -import org.w3c.css.sac.CharacterDataSelector; -import org.w3c.css.sac.Condition; -import org.w3c.css.sac.ConditionalSelector; -import org.w3c.css.sac.DescendantSelector; -import org.w3c.css.sac.ElementSelector; -import org.w3c.css.sac.NegativeSelector; -import org.w3c.css.sac.ProcessingInstructionSelector; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorFactory; -import org.w3c.css.sac.SiblingSelector; -import org.w3c.css.sac.SimpleSelector; - -/** - * This class implements the {@link org.w3c.css.sac.SelectorFactory} interface. - */ -public class CSSSelectorFactoryImpl implements SelectorFactory { - - private static final String NOT_IMPLEMENTED_IN_CSS2 = "Not implemented in CSS2"; //$NON-NLS-1$ - - /** - * The instance of this class. - */ - public static final SelectorFactory INSTANCE = new CSSSelectorFactoryImpl(); - - /** - * This class does not need to be instantiated. - */ - protected CSSSelectorFactoryImpl() { - } - - /** - * SAC: Implements {@link - * SelectorFactory#createConditionalSelector(SimpleSelector,Condition)}. - */ - @Override - public ConditionalSelector createConditionalSelector( - SimpleSelector selector, Condition condition) throws CSSException { - return new CSSConditionalSelectorImpl(selector, condition); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createAnyNodeSelector()}. - */ - @Override - public SimpleSelector createAnyNodeSelector() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createRootNodeSelector()}. - */ - @Override - public SimpleSelector createRootNodeSelector() throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createNegativeSelector(SimpleSelector)}. - */ - @Override - public NegativeSelector createNegativeSelector(SimpleSelector selector) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createElementSelector(String,String)}. - */ - @Override - public ElementSelector createElementSelector(String namespaceURI, - String tagName) throws CSSException { - return new CSSElementSelectorImpl(namespaceURI, tagName); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createTextNodeSelector(String)}. - */ - @Override - public CharacterDataSelector createTextNodeSelector(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createCDataSectionSelector(String)}. - */ - @Override - public CharacterDataSelector createCDataSectionSelector(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createProcessingInstructionSelector(String,String)}. - */ - @Override - public ProcessingInstructionSelector createProcessingInstructionSelector( - String target, String data) throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * org.w3c.css.sac.SelectorFactory#createCommentSelector(String)}. - */ - @Override - public CharacterDataSelector createCommentSelector(String data) - throws CSSException { - throw new CSSException(NOT_IMPLEMENTED_IN_CSS2); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createPseudoElementSelector(String,String)}. - */ - @Override - public ElementSelector createPseudoElementSelector(String namespaceURI, - String pseudoName) throws CSSException { - return new CSSPseudoElementSelectorImpl(namespaceURI, pseudoName); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createDescendantSelector(Selector,SimpleSelector)}. - */ - @Override - public DescendantSelector createDescendantSelector(Selector parent, - SimpleSelector descendant) throws CSSException { - return new CSSDescendantSelectorImpl(parent, descendant); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createChildSelector(Selector,SimpleSelector)}. - */ - @Override - public DescendantSelector createChildSelector(Selector parent, - SimpleSelector child) throws CSSException { - return new CSSChildSelectorImpl(parent, child); - } - - /** - * SAC: Implements {@link - * SelectorFactory#createDirectAdjacentSelector(short,Selector,SimpleSelector)}. - */ - @Override - public SiblingSelector createDirectAdjacentSelector(short nodeType, - Selector child, SimpleSelector directAdjacent) throws CSSException { - return new CSSDirectAdjacentSelectorImpl(nodeType, child, - directAdjacent); - } -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java deleted file mode 100644 index edc785b6760..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedCondition.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Condition; -import org.w3c.dom.Element; - -/** - * This interface provides additional features to the - * {@link org.w3c.css.sac.Condition} interface. - */ -public interface ExtendedCondition extends Condition { - - /** - * Tests whether this condition matches the given element. - */ - boolean match(Element e, String pseudoE); - - /** - * Returns the specificity of this condition. - */ - int getSpecificity(); - - /** - * Fills the given set with the attribute names found in this selector. - */ - void fillAttributeSet(Set attrSet); -} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java deleted file mode 100644 index b30db5d0db1..00000000000 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/sac/ExtendedSelector.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - - Copyright 2002, 2015 The Apache Software Foundation - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - */ - -/* This class copied from org.apache.batik.css.engine.sac */ - -package org.eclipse.e4.ui.css.core.impl.sac; - -import java.util.Set; -import org.w3c.css.sac.Selector; -import org.w3c.dom.Element; -import org.w3c.dom.Node; - -/** - * This interface extends the {@link org.w3c.css.sac.Selector}. - */ -public interface ExtendedSelector extends Selector { - - default boolean match(Element e, Node[] ancestors, int parentIndex, String pseudoE) { - return match(e, pseudoE); - } - - /** - * Tests whether this selector matches the given element. - */ - boolean match(Element e, String pseudoE); - - /** - * Returns the specificity of this selector. - */ - int getSpecificity(); - - /** - * Fills the given set with the attribute names found in this selector. - */ - void fillAttributeSet(Set attrSet); -} diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java index 8bfc22e01c4..7adcce44634 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java @@ -139,17 +139,22 @@ private boolean isApplicableToReset(WidgetElement element) { @Override public void reapply() { - Shell[] shells = display.getShells(); - for (Shell s : shells) { - try { - s.setRedraw(false); - s.reskin(SWT.ALL); - applyStyles(s, true); - } catch (Exception e) { - ILog.of(getClass()).error(e.getMessage(), e); - } finally { - s.setRedraw(true); + startStylingSession(); + try { + Shell[] shells = display.getShells(); + for (Shell s : shells) { + try { + s.setRedraw(false); + s.reskin(SWT.ALL); + applyStyles(s, true); + } catch (Exception e) { + ILog.of(getClass()).error(e.getMessage(), e); + } finally { + s.setRedraw(true); + } } + } finally { + stopStylingSession(); } } } diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java index fa16ea5395e..31916477fd6 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/CSSEngineTest.java @@ -23,10 +23,10 @@ import org.eclipse.e4.ui.css.core.engine.CSSEngine; import org.eclipse.e4.ui.css.core.impl.engine.CSSEngineImpl; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors.Selector; import org.eclipse.e4.ui.tests.css.core.util.TestElement; import org.junit.jupiter.api.Test; -import org.w3c.css.sac.Selector; -import org.w3c.css.sac.SelectorList; import org.w3c.dom.Element; class CSSEngineTest { @@ -55,7 +55,7 @@ private static Selector parse(CSSEngine engine, String selector) throws Exceptio @Test void testSelectorMatch() throws Exception { TestCSSEngine engine = new TestCSSEngine(); - SelectorList list = engine.parseSelectors("Date"); + Selectors.SelectorList list = engine.parseSelectors("Date"); engine.setElementProvider((element, engine1) -> new TestElement(element.getClass().getSimpleName(), engine1)); assertFalse(engine.matches(list.item(0), new Object(), null)); @@ -249,7 +249,7 @@ void testNegativeMatch() throws Exception { @Test void testSelectorListMatch() throws Exception { TestCSSEngine engine = new TestCSSEngine(); - SelectorList list = engine.parseSelectors(".a, .b"); + Selectors.SelectorList list = engine.parseSelectors(".a, .b"); TestElement a = createElement(engine, "Button", "a", null); TestElement b = createElement(engine, "Button", "b", null); TestElement c = createElement(engine, "Button", "c", null); @@ -274,9 +274,9 @@ void testTagNameCaseSensitivity() throws Exception { assertFalse(engine.matches(lower, capitalElement, null)); } - private static boolean matchesAny(CSSEngine engine, SelectorList list, Element element) { - for (int i = 0; i < list.getLength(); i++) { - if (engine.matches(list.item(i), element, null)) { + private static boolean matchesAny(CSSEngine engine, Selectors.SelectorList list, Element element) { + for (Selector selector : list.alternatives()) { + if (engine.matches(selector, element, null)) { return true; } } diff --git a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java index f3419a83eb0..43a3be1899b 100644 --- a/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java +++ b/tests/org.eclipse.e4.ui.tests.css.core/src/org/eclipse/e4/ui/tests/css/core/parser/SelectorTest.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2013, 2014 IBM Corporation and others. + * Copyright (c) 2013, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -21,11 +21,11 @@ import java.io.IOException; import org.eclipse.e4.ui.css.core.engine.CSSEngine; +import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.eclipse.e4.ui.tests.css.core.util.ParserTestUtil; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.w3c.css.sac.CSSParseException; -import org.w3c.css.sac.SelectorList; public class SelectorTest { private CSSEngine engine; @@ -37,35 +37,37 @@ public void setUp() throws Exception { @Test void testSimpleSelector() throws Exception { - SelectorList list = engine.parseSelectors("Type1"); + Selectors.SelectorList list = engine.parseSelectors("Type1"); assertNotNull(list); assertEquals(1, list.getLength()); - assertEquals("Type1", list.item(0).toString()); + assertEquals("Type1", list.item(0).text()); } @Test void testMultipleSelectors() throws Exception { - SelectorList list = engine.parseSelectors("Type1, Type2"); + Selectors.SelectorList list = engine.parseSelectors("Type1, Type2"); assertNotNull(list); assertEquals(2, list.getLength()); - assertEquals("Type1", list.item(0).toString()); - assertEquals("Type2", list.item(1).toString()); + assertEquals("Type1", list.item(0).text()); + assertEquals("Type2", list.item(1).text()); } @Test void testClassSelector() throws Exception { - SelectorList list = engine.parseSelectors(".Class1"); + Selectors.SelectorList list = engine.parseSelectors(".Class1"); assertNotNull(list); assertEquals(1, list.getLength()); - assertEquals("*[class=\"Class1\"]", list.item(0).toString()); + assertEquals(".Class1", list.item(0).text()); } @Test void testAttributeSelector() throws Exception { - SelectorList list = engine.parseSelectors("*[class='Class1']"); + Selectors.SelectorList list = engine.parseSelectors("*[class='Class1']"); assertNotNull(list); assertEquals(1, list.getLength()); - assertEquals("*[class=\"Class1\"]", list.item(0).toString()); + // The Universal selector ('*') is folded away since the AttributeSelector + // alone carries the full match condition. + assertEquals("[class='Class1']", list.item(0).text()); } @Test From 28e33abb6557eb75d6d15b4145e9f6d97999f0d7 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 21 May 2026 21:46:12 +0200 Subject: [PATCH 17/19] Add CSS engine tracing for theme-swap performance analysis Introduces a small CSSCorePolicy class with AtomicLong counters and ns totals for the engine hot paths (applyStyles, getComputedStyle, selectorMatch, applyStyleDeclaration, applyCSSProperty, handlerLookup, handlerInvoke, applyInlineStyle, parseStyleSheet, reset, plus reapply and reskin on the SWT side and the per-block buckets inside applyStyles: entry, pseudoCascade, childRecursion). CSSEngineImpl, ViewCSSImpl, and CSSSWTEngineImpl wrap their internal methods with nanoTime fences guarded by a single volatile DEBUG_PERF flag, so the runtime cost is one volatile read and two AtomicLong updates per call when tracing is off (effectively free). The flag is driven by the standard Eclipse .options mechanism: org.eclipse.e4.ui.css.core/debug = true plus org.eclipse.e4.ui.css.core/debug/perf = true in a launch config's Tracing tab. CSSCorePolicy.setPerfEnabled(boolean) is also exposed so tests can toggle it programmatically around a measurement window, and CSSCorePolicy.summary() formats the accumulated buckets as one hierarchical block (counts, total ms, avg us) for log capture. The x-friends list on org.eclipse.e4.ui.css.core.impl.engine grows org.eclipse.ui.tests.performance so the perf test bundle can read the counters; nothing in production consumes the API. Contributes to #3980 --- bundles/org.eclipse.e4.ui.css.core/.options | 13 ++ .../META-INF/MANIFEST.MF | 2 +- .../build.properties | 3 +- .../e4/ui/css/core/impl/dom/ViewCSSImpl.java | 23 ++- .../css/core/impl/engine/CSSCorePolicy.java | 147 ++++++++++++++++++ .../css/core/impl/engine/CSSEngineImpl.java | 97 +++++++++++- .../ui/css/swt/engine/CSSSWTEngineImpl.java | 12 ++ 7 files changed, 292 insertions(+), 5 deletions(-) create mode 100644 bundles/org.eclipse.e4.ui.css.core/.options create mode 100644 bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java diff --git a/bundles/org.eclipse.e4.ui.css.core/.options b/bundles/org.eclipse.e4.ui.css.core/.options new file mode 100644 index 00000000000..a817bf6e9e9 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/.options @@ -0,0 +1,13 @@ +# Debugging options for the org.eclipse.e4.ui.css.core plugin. + +# Master switch. Must be true for any of the more specific debug +# options below to take effect. +org.eclipse.e4.ui.css.core/debug=false + +# Accumulate per-phase timings inside the CSS engine (applyStyles, +# getComputedStyle, selector match, applyStyleDeclaration, +# applyCSSProperty) and print a summary to stdout after each +# top-level applyStyles call. Useful for measuring where time goes +# during a theme swap. Has no effect unless the master "debug" +# option is also true. +org.eclipse.e4.ui.css.core/debug/perf=false diff --git a/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF index 27dfb2acb81..cb1459510d9 100644 --- a/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.e4.ui.css.core/META-INF/MANIFEST.MF @@ -30,7 +30,7 @@ Export-Package: org.eclipse.e4.ui.css.core;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom.parsers;x-internal:=true, org.eclipse.e4.ui.css.core.impl.dom.properties;x-friends:="org.eclipse.e4.ui.css.swt", - org.eclipse.e4.ui.css.core.impl.engine;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.swt", + org.eclipse.e4.ui.css.core.impl.engine;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.swt,org.eclipse.ui.tests.performance", org.eclipse.e4.ui.css.core.impl.engine.selector;x-friends:="org.eclipse.e4.ui.tests.css.core", org.eclipse.e4.ui.css.core.impl.sac;x-internal:=true, org.eclipse.e4.ui.css.core.resources;x-friends:="org.eclipse.e4.ui.css.swt,org.eclipse.e4.ui.workbench.renderers.swt", diff --git a/bundles/org.eclipse.e4.ui.css.core/build.properties b/bundles/org.eclipse.e4.ui.css.core/build.properties index b845fbdbe42..883afd2dc17 100644 --- a/bundles/org.eclipse.e4.ui.css.core/build.properties +++ b/bundles/org.eclipse.e4.ui.css.core/build.properties @@ -16,7 +16,8 @@ bin.includes = META-INF/,\ .,\ plugin.xml,\ about.html,\ - plugin.properties + plugin.properties,\ + .options src.includes = schema/,\ apache_about_files/,\ about.html diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java index aa1a5c8a92c..080e3deeb10 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/dom/ViewCSSImpl.java @@ -21,6 +21,7 @@ import java.util.List; import org.eclipse.e4.ui.css.core.dom.ExtendedCSSRule; import org.eclipse.e4.ui.css.core.dom.ExtendedDocumentCSS; +import org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy; import org.eclipse.e4.ui.css.core.impl.engine.selector.SelectorMatcher; import org.eclipse.e4.ui.css.core.impl.engine.selector.Selectors; import org.w3c.dom.Element; @@ -110,6 +111,20 @@ private List getCombinedRules() { } private CSSStyleDeclaration getComputedStyle(List ruleList, Element elt, String pseudoElt) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + return getComputedStyleInternal(ruleList, elt, pseudoElt, trace); + } finally { + if (trace) { + CSSCorePolicy.getComputedStyleNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.getComputedStyleCount.incrementAndGet(); + } + } + } + + private CSSStyleDeclaration getComputedStyleInternal(List ruleList, Element elt, String pseudoElt, + boolean trace) { List styleDeclarations = null; StyleWrapper firstStyleDeclaration = null; int position = 0; @@ -132,7 +147,13 @@ private CSSStyleDeclaration getComputedStyle(List ruleList, Element elt ExtendedCSSRule r = (ExtendedCSSRule) rule; Selectors.SelectorList selectorList = r.getSelectorList(); for (Selectors.Selector selector : selectorList.alternatives()) { - if (SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0)) { + long m0 = trace ? System.nanoTime() : 0; + boolean matched = SelectorMatcher.matches(selector, elt, pseudoElt, hierarchy, 0); + if (trace) { + CSSCorePolicy.selectorMatchNs.addAndGet(System.nanoTime() - m0); + CSSCorePolicy.selectorMatchCount.incrementAndGet(); + } + if (matched) { CSSStyleDeclaration style = styleRule.getStyle(); int specificity = selector.specificity(); StyleWrapper wrapper = new StyleWrapper(style, specificity, position++); diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java new file mode 100644 index 00000000000..0ae7b273626 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSCorePolicy.java @@ -0,0 +1,147 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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.e4.ui.css.core.impl.engine; + +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.core.runtime.Platform; + +/** + * Debug / tracing policy for the CSS core engine. Reads its booleans from the + * standard Eclipse {@code .options} file at the bundle root (visible in the + * Tracing tab of an Eclipse run configuration). + * + *

+ * When {@link #DEBUG_PERF} is true, the engine accumulates per-phase wall + * times and prints a summary after each top-level applyStyles call. Tests may + * also flip the flag programmatically via {@link #setPerfEnabled(boolean)}. + *

+ */ +public final class CSSCorePolicy { + + private static final String PLUGIN_ID = "org.eclipse.e4.ui.css.core"; //$NON-NLS-1$ + + public static volatile boolean DEBUG = false; + public static volatile boolean DEBUG_PERF = false; + + static { + if (option("/debug")) { //$NON-NLS-1$ + DEBUG = true; + DEBUG_PERF = option("/debug/perf"); //$NON-NLS-1$ + } + } + + private static boolean option(String suffix) { + return "true".equalsIgnoreCase(Platform.getDebugOption(PLUGIN_ID + suffix)); //$NON-NLS-1$ + } + + public static void setPerfEnabled(boolean enabled) { + DEBUG = enabled || DEBUG; + DEBUG_PERF = enabled; + } + + // Counters and total nanoseconds for the instrumented hot paths. + public static final AtomicLong applyStylesCount = new AtomicLong(); + public static final AtomicLong applyStylesNs = new AtomicLong(); + public static final AtomicLong getComputedStyleCount = new AtomicLong(); + public static final AtomicLong getComputedStyleNs = new AtomicLong(); + public static final AtomicLong selectorMatchCount = new AtomicLong(); + public static final AtomicLong selectorMatchNs = new AtomicLong(); + public static final AtomicLong applyStyleDeclarationCount = new AtomicLong(); + public static final AtomicLong applyStyleDeclarationNs = new AtomicLong(); + public static final AtomicLong applyCSSPropertyCount = new AtomicLong(); + public static final AtomicLong applyCSSPropertyNs = new AtomicLong(); + public static final AtomicLong parseStyleSheetCount = new AtomicLong(); + public static final AtomicLong parseStyleSheetNs = new AtomicLong(); + public static final AtomicLong resetCount = new AtomicLong(); + public static final AtomicLong resetNs = new AtomicLong(); + public static final AtomicLong applyInlineStyleCount = new AtomicLong(); + public static final AtomicLong applyInlineStyleNs = new AtomicLong(); + public static final AtomicLong applyStylesEntryNs = new AtomicLong(); + public static final AtomicLong pseudoCascadeNs = new AtomicLong(); + public static final AtomicLong childRecursionNs = new AtomicLong(); + public static final AtomicLong handlerInvokeCount = new AtomicLong(); + public static final AtomicLong handlerInvokeNs = new AtomicLong(); + public static final AtomicLong handlerLookupCount = new AtomicLong(); + public static final AtomicLong handlerLookupNs = new AtomicLong(); + public static final AtomicLong reapplyCount = new AtomicLong(); + public static final AtomicLong reapplyNs = new AtomicLong(); + public static final AtomicLong reskinCount = new AtomicLong(); + public static final AtomicLong reskinNs = new AtomicLong(); + + public static void reset() { + applyStylesCount.set(0); + applyStylesNs.set(0); + getComputedStyleCount.set(0); + getComputedStyleNs.set(0); + selectorMatchCount.set(0); + selectorMatchNs.set(0); + applyStyleDeclarationCount.set(0); + applyStyleDeclarationNs.set(0); + applyCSSPropertyCount.set(0); + applyCSSPropertyNs.set(0); + parseStyleSheetCount.set(0); + parseStyleSheetNs.set(0); + resetCount.set(0); + resetNs.set(0); + applyInlineStyleCount.set(0); + applyInlineStyleNs.set(0); + applyStylesEntryNs.set(0); + pseudoCascadeNs.set(0); + childRecursionNs.set(0); + handlerInvokeCount.set(0); + handlerInvokeNs.set(0); + handlerLookupCount.set(0); + handlerLookupNs.set(0); + reapplyCount.set(0); + reapplyNs.set(0); + reskinCount.set(0); + reskinNs.set(0); + } + + public static String summary() { + StringBuilder sb = new StringBuilder(); + sb.append("[CSS-PERF-TRACE]\n"); //$NON-NLS-1$ + line(sb, "reapply (top) ", reapplyCount, reapplyNs); //$NON-NLS-1$ + line(sb, " reskin ", reskinCount, reskinNs); //$NON-NLS-1$ + line(sb, "parseStyleSheet ", parseStyleSheetCount, parseStyleSheetNs); //$NON-NLS-1$ + line(sb, "engine.reset ", resetCount, resetNs); //$NON-NLS-1$ + line(sb, "applyStyles ", applyStylesCount, applyStylesNs); //$NON-NLS-1$ + line(sb, "getComputedStyle ", getComputedStyleCount, getComputedStyleNs); //$NON-NLS-1$ + line(sb, "selectorMatch ", selectorMatchCount, selectorMatchNs); //$NON-NLS-1$ + line(sb, "applyStyleDeclar.", applyStyleDeclarationCount, applyStyleDeclarationNs); //$NON-NLS-1$ + line(sb, "applyCSSProperty ", applyCSSPropertyCount, applyCSSPropertyNs); //$NON-NLS-1$ + line(sb, " handlerLookup ", handlerLookupCount, handlerLookupNs); //$NON-NLS-1$ + line(sb, " handlerInvoke ", handlerInvokeCount, handlerInvokeNs); //$NON-NLS-1$ + line(sb, "applyInlineStyle ", applyInlineStyleCount, applyInlineStyleNs); //$NON-NLS-1$ + lineTotalOnly(sb, "applyStyles entry", applyStylesEntryNs); //$NON-NLS-1$ + lineTotalOnly(sb, "pseudoCascade ", pseudoCascadeNs); //$NON-NLS-1$ + lineTotalOnly(sb, "childRecursion ", childRecursionNs); //$NON-NLS-1$ + return sb.toString(); + } + + private static void lineTotalOnly(StringBuilder sb, String label, AtomicLong ns) { + double totalMs = ns.get() / 1_000_000.0; + sb.append(String.format(" %s total=%9.2fms%n", label, totalMs)); //$NON-NLS-1$ + } + + private static void line(StringBuilder sb, String label, AtomicLong count, AtomicLong ns) { + long c = count.get(); + long n = ns.get(); + double totalMs = n / 1_000_000.0; + double avgUs = c == 0 ? 0 : n / 1000.0 / c; + sb.append(String.format(" %s count=%9d total=%9.2fms avg=%9.2fus%n", label, c, totalMs, avgUs)); //$NON-NLS-1$ + } + + private CSSCorePolicy() { + // statics only + } +} diff --git a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java index 66e7ede4f84..993d908b882 100644 --- a/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.core/src/org/eclipse/e4/ui/css/core/impl/engine/CSSEngineImpl.java @@ -189,6 +189,19 @@ public StyleSheet parseStyleSheet(InputStream stream) throws IOException { @Override public StyleSheet parseStyleSheet(InputSource source) throws IOException { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + return parseStyleSheetInternal(source); + } finally { + if (trace) { + CSSCorePolicy.parseStyleSheetNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.parseStyleSheetCount.incrementAndGet(); + } + } + } + + private StyleSheet parseStyleSheetInternal(InputSource source) throws IOException { // Check that CharacterStream or ByteStream is not null checkInputSource(source); CSSParser parser = makeCSSParser(); @@ -407,10 +420,31 @@ public void applyStyles(Object element, boolean applyStylesToChildNodes) { @Override public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + applyStylesInternal(element, applyStylesToChildNodes, computeDefaultStyle); + } finally { + if (trace) { + CSSCorePolicy.applyStylesNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.applyStylesCount.incrementAndGet(); + } + } + } + + private void applyStylesInternal(Object element, boolean applyStylesToChildNodes, boolean computeDefaultStyle) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long entryT0 = trace ? System.nanoTime() : 0; Element elt = getElement(element); if (elt == null || !isVisible(elt)) { + if (trace) { + CSSCorePolicy.applyStylesEntryNs.addAndGet(System.nanoTime() - entryT0); + } return; } + if (trace) { + CSSCorePolicy.applyStylesEntryNs.addAndGet(System.nanoTime() - entryT0); + } if (isElementStyled(element)) { if (applyStylesToChildNodes) { @@ -444,6 +478,7 @@ public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean /* * Manage static pseudo instances */ + long pseudoT0 = trace ? System.nanoTime() : 0; String[] pseudoInstances = getStaticPseudoInstances(elt); if (pseudoInstances != null && pseudoInstances.length > 0) { // there are static pseudo instances defined, loop for it and @@ -467,18 +502,27 @@ public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean } } } + if (trace) { + CSSCorePolicy.pseudoCascadeNs.addAndGet(System.nanoTime() - pseudoT0); + } if (style != null) { applyStyleDeclaration(elt, style, null); } + long inlineT0 = trace ? System.nanoTime() : 0; try { // Apply inline style applyInlineStyle(elt, false); } catch (Exception e) { handleExceptions(e); } + if (trace) { + CSSCorePolicy.applyInlineStyleNs.addAndGet(System.nanoTime() - inlineT0); + CSSCorePolicy.applyInlineStyleCount.incrementAndGet(); + } if (applyStylesToChildNodes) { + long childT0 = trace ? System.nanoTime() : 0; /* * Style all children recursive. */ @@ -489,6 +533,9 @@ public void applyStyles(Object element, boolean applyStylesToChildNodes, boolean processNodeList(nodes, this::applyStyles, applyStylesToChildNodes); onStylesAppliedToChildNodes(elt, nodes); } + if (trace) { + CSSCorePolicy.childRecursionNs.addAndGet(System.nanoTime() - childT0); + } } } @@ -591,6 +638,19 @@ protected void onStylesAppliedToChildNodes(Element element, NodeList nodes) { @Override public void applyStyleDeclaration(Object element, CSSStyleDeclaration style, String pseudo) { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + applyStyleDeclarationInternal(element, style, pseudo); + } finally { + if (trace) { + CSSCorePolicy.applyStyleDeclarationNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.applyStyleDeclarationCount.incrementAndGet(); + } + } + } + + private void applyStyleDeclarationInternal(Object element, CSSStyleDeclaration style, String pseudo) { // Apply style boolean avoidanceCacheInstalled = currentCSSPropertiesApplied == null; if (avoidanceCacheInstalled) { @@ -760,6 +820,20 @@ public void applyDefaultStyleDeclaration(Object element, boolean applyStylesToCh @Override public ICSSPropertyHandler applyCSSProperty(Object element, String property, CSSValue value, String pseudo) throws Exception { + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + return applyCSSPropertyInternal(element, property, value, pseudo); + } finally { + if (trace) { + CSSCorePolicy.applyCSSPropertyNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.applyCSSPropertyCount.incrementAndGet(); + } + } + } + + private ICSSPropertyHandler applyCSSPropertyInternal(Object element, String property, CSSValue value, String pseudo) + throws Exception { if (currentCSSPropertiesApplied != null && currentCSSPropertiesApplied.containsKey(property)) { // CSS Property was already applied, ignore it. return null; @@ -777,14 +851,25 @@ public ICSSPropertyHandler applyCSSProperty(Object element, String property, CSS value = parsePropertyValue(parentValueString); } + final boolean trace = CSSCorePolicy.DEBUG_PERF; for (ICSSPropertyHandlerProvider provider : propertyHandlerProviders) { + long lookupT0 = trace ? System.nanoTime() : 0; Collection handlers = provider.getCSSPropertyHandlers(element, property); + if (trace) { + CSSCorePolicy.handlerLookupNs.addAndGet(System.nanoTime() - lookupT0); + CSSCorePolicy.handlerLookupCount.incrementAndGet(); + } if (handlers == null) { continue; } for (ICSSPropertyHandler handler : handlers) { try { + long invokeT0 = trace ? System.nanoTime() : 0; boolean result = handler.applyCSSProperty(element, property, value, pseudo, this); + if (trace) { + CSSCorePolicy.handlerInvokeNs.addAndGet(System.nanoTime() - invokeT0); + CSSCorePolicy.handlerInvokeCount.incrementAndGet(); + } if (result) { // Add CSS Property to flag that this CSS Property was // applied. @@ -1064,8 +1149,16 @@ public void dispose() { @Override public void reset() { - // Remove All Style Sheets - documentCSS.removeAllStyleSheets(); + final boolean trace = CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; + try { + documentCSS.removeAllStyleSheets(); + } finally { + if (trace) { + CSSCorePolicy.resetNs.addAndGet(System.nanoTime() - t0); + CSSCorePolicy.resetCount.incrementAndGet(); + } + } } /*--------------- Resources Registry -----------------*/ diff --git a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java index 7adcce44634..01da9199385 100644 --- a/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java +++ b/bundles/org.eclipse.e4.ui.css.swt/src/org/eclipse/e4/ui/css/swt/engine/CSSSWTEngineImpl.java @@ -139,13 +139,21 @@ private boolean isApplicableToReset(WidgetElement element) { @Override public void reapply() { + final boolean trace = org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.DEBUG_PERF; + long t0 = trace ? System.nanoTime() : 0; startStylingSession(); try { Shell[] shells = display.getShells(); for (Shell s : shells) { try { s.setRedraw(false); + long r0 = trace ? System.nanoTime() : 0; s.reskin(SWT.ALL); + if (trace) { + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reskinNs + .addAndGet(System.nanoTime() - r0); + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reskinCount.incrementAndGet(); + } applyStyles(s, true); } catch (Exception e) { ILog.of(getClass()).error(e.getMessage(), e); @@ -155,6 +163,10 @@ public void reapply() { } } finally { stopStylingSession(); + if (trace) { + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reapplyNs.addAndGet(System.nanoTime() - t0); + org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy.reapplyCount.incrementAndGet(); + } } } } From 497c1ac7163a6f52c4335650c6679d510d041f7c Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 21 May 2026 21:46:33 +0200 Subject: [PATCH 18/19] Add CssThemeSwapPerformanceTest workbench benchmark A JUnit 5 performance test that exercises the active workbench window with a workload similar to a busy IDE session and then swaps the workbench theme between Light and Dark several times, recording the wall-clock time of each swap. Setup creates a JDT project with 20 Java compilation units, opens an editor on each one, shows eleven standard workbench views (Project Explorer, Outline, Problems, Tasks, Console, Search, Markers, Bookmarks, Properties, Progress, Error Log), expands the Project Explorer to a depth of three and activates it, and finally shows a new CssStressView that lays out around 4000 SWT widgets across fourteen widget kinds (Label, Button push/check/radio, Text, Combo, Spinner, List, Link, ProgressBar, CLabel, CCombo, Scale, DateTime, plus periodic ToolBar, Tree, Table, ExpandBar containers) inside a ScrolledComposite. The shell is maximised and the test waits five seconds for layout, paint, and initial CSS application to settle. The measurement loop runs three warm-up swaps then ten timed swaps, processing the event queue between iterations, and reports min, median, p95, max, and mean of the timed window. While the timed window runs, the test flips CSSCorePolicy.setPerfEnabled(true) and prints the engine trace summary afterwards, so a single run captures both the wall-clock swap times and the per-phase breakdown used to chase the theme-swap regression on the CSS engine rework branch. The test bundle picks up Require-Bundle entries for the e4 CSS engine, the workbench theme APIs, JDT core, and JDT launching so the workload setup and the trace API are reachable. plugin.xml registers the view. Contributes to #3980 --- .../META-INF/MANIFEST.MF | 8 +- .../plugin.xml | 11 +- .../CssThemeSwapPerformanceTest.java | 313 ++++++++++++++++++ .../performance/parts/CssStressViewPart.java | 209 ++++++++++++ 4 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java create mode 100644 tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java diff --git a/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF index 8c0d2929e3d..f7adc266c7a 100644 --- a/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.ui.tests.performance/META-INF/MANIFEST.MF @@ -19,7 +19,13 @@ Require-Bundle: org.eclipse.ui;bundle-version="3.208.0", org.eclipse.e4.core.contexts, org.eclipse.ui.navigator, org.eclipse.ui.navigator.resources, - org.eclipse.ui.genericeditor + org.eclipse.ui.genericeditor, + org.eclipse.e4.ui.css.core, + org.eclipse.e4.ui.css.swt, + org.eclipse.e4.ui.css.swt.theme, + org.eclipse.ui.themes, + org.eclipse.jdt.core, + org.eclipse.jdt.launching Import-Package: org.junit.jupiter.api;version="[5.14.0,6.0.0)", org.junit.jupiter.api.extension;version="[5.14.0,6.0.0)", org.junit.jupiter.params;version="[5.14.0,6.0.0)", diff --git a/tests/org.eclipse.ui.tests.performance/plugin.xml b/tests/org.eclipse.ui.tests.performance/plugin.xml index 8f051aab1f6..7116adc1286 100644 --- a/tests/org.eclipse.ui.tests.performance/plugin.xml +++ b/tests/org.eclipse.ui.tests.performance/plugin.xml @@ -69,6 +69,13 @@ id="org.eclipse.ui.tests.performance.problemsView" name="Performance Problems View"/> - - + + + + + diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java new file mode 100644 index 00000000000..0cb50abbbe9 --- /dev/null +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/CssThemeSwapPerformanceTest.java @@ -0,0 +1,313 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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.tests.performance; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.processEvents; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IFolder; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.e4.ui.css.core.impl.engine.CSSCorePolicy; +import org.eclipse.e4.ui.css.swt.theme.IThemeEngine; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.launching.JavaRuntime; +import org.eclipse.swt.widgets.Display; +import org.eclipse.ui.IPerspectiveDescriptor; +import org.eclipse.ui.IViewPart; +import org.eclipse.ui.IWorkbenchPage; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.navigator.CommonNavigator; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +/** + * Benchmark for the workbench CSS theme engine. Opens a real workbench with a + * Java project, 20 editors, a CSS stress view (~4000 mixed widgets), and a set + * of standard views, then times repeated theme swaps between the e4 light and + * dark themes. + * + * Not part of the default test run. Invoke explicitly via + * mvn verify -pl :org.eclipse.ui.tests.performance -Pbuild-individual-bundles \ + * -Dtest=CssThemeSwapPerformanceTest -DskipTests=false + */ +@Tag("performance") +public class CssThemeSwapPerformanceTest { + + private static final String LIGHT_THEME = "org.eclipse.e4.ui.css.theme.e4_default"; + private static final String DARK_THEME = "org.eclipse.e4.ui.css.theme.e4_dark"; + private static final String THEME_ENGINE_KEY = "org.eclipse.e4.ui.css.swt.theme"; + + private static final String CSS_STRESS_VIEW = "org.eclipse.ui.tests.performance.cssStressView"; + private static final String PDE_PERSPECTIVE = "org.eclipse.pde.ui.PDEPerspective"; + + /** + * Views opened to make the workbench widget tree look like a real session. + * Some IDs may not be registered in the test runtime; failures are tolerated. + */ + private static final List STANDARD_VIEWS = List.of( + "org.eclipse.ui.navigator.ProjectExplorer", + "org.eclipse.ui.views.ProblemView", + "org.eclipse.ui.views.ContentOutline", + "org.eclipse.ui.views.TaskList", + "org.eclipse.ui.views.BookmarkView", + "org.eclipse.ui.views.AllMarkersView", + "org.eclipse.ui.views.ProgressView", + "org.eclipse.ui.views.PropertySheet", + "org.eclipse.search.ui.views.SearchView", + "org.eclipse.ui.console.ConsoleView", + "org.eclipse.pde.runtime.LogView", + "org.eclipse.team.ui.GenericHistoryView", + "org.eclipse.team.sync.views.SynchronizeView", + "org.eclipse.help.ui.HelpView"); + + private static final int EDITOR_COUNT = 20; + private static final int WARMUP_ROUNDS = 3; + private static final int MEASURE_ROUNDS = 10; + private static final long PRE_SWAP_WAIT_MS = 5_000; + + private IWorkbenchWindow window; + private final List openedViews = new ArrayList<>(); + private IProject project; + + @AfterEach + public void cleanup() throws Exception { + if (window != null) { + IWorkbenchPage page = window.getActivePage(); + if (page != null) { + page.closeAllEditors(false); + for (IViewPart v : openedViews) { + page.hideView(v); + } + processEvents(); + } + } + if (project != null && project.exists()) { + project.delete(true, true, null); + } + } + + @Test + public void themeSwap_realWorkbench() throws Exception { + window = PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + assertNotNull(window, "Active workbench window must be available"); + window.getShell().setMaximized(true); + IWorkbenchPage page = window.getActivePage(); + + IPerspectiveDescriptor pde = PlatformUI.getWorkbench().getPerspectiveRegistry() + .findPerspectiveWithId(PDE_PERSPECTIVE); + if (pde != null) { + page.setPerspective(pde); + System.out.println("[CSS-PERF] switched to PDE perspective"); + } else { + System.out.println("[CSS-PERF] PDE perspective not available; keeping current (" + + (page.getPerspective() == null ? "" : page.getPerspective().getId()) + ")"); + } + processEvents(); + + List files = createJavaProjectWithFiles("CssPerf", EDITOR_COUNT); + for (IFile f : files) { + IDE.openEditor(page, f); + } + System.out.println("[CSS-PERF] opened " + files.size() + " editors"); + + IViewPart cssView = page.showView(CSS_STRESS_VIEW); + openedViews.add(cssView); + IViewPart projectExplorer = null; + int viewsOpened = 1; + for (String id : STANDARD_VIEWS) { + try { + IViewPart v = page.showView(id); + openedViews.add(v); + if ("org.eclipse.ui.navigator.ProjectExplorer".equals(id)) { + projectExplorer = v; + } + viewsOpened++; + } catch (Exception e) { + System.out.println("[CSS-PERF] skipping view " + id + ": " + e.getMessage()); + } + } + System.out.println("[CSS-PERF] opened " + viewsOpened + " views"); + + // keep the CSS stress view on top of its stack even after activating + // the project explorer + page.showView(CSS_STRESS_VIEW); + + if (projectExplorer != null) { + if (projectExplorer instanceof CommonNavigator nav) { + nav.getCommonViewer().expandToLevel(3); + } + page.activate(projectExplorer); + } + processEvents(); + + Display display = window.getShell().getDisplay(); + + System.out.println("[CSS-PERF] workbench populated; waiting " + + (PRE_SWAP_WAIT_MS / 1000) + "s for things to settle before the theme swap..."); + pumpEvents(display, PRE_SWAP_WAIT_MS); + + IThemeEngine themeEngine = (IThemeEngine) display.getData(THEME_ENGINE_KEY); + assertNotNull(themeEngine, "IThemeEngine must be available on the workbench Display"); + + System.out.println("[CSS-PERF] registered themes:"); + for (var t : themeEngine.getThemes()) { + System.out.println("[CSS-PERF] - " + t.getId() + " (" + t.getLabel() + ")"); + } + System.out.println("[CSS-PERF] active theme: " + + (themeEngine.getActiveTheme() == null ? "" : themeEngine.getActiveTheme().getId())); + + boolean hasDark = themeEngine.getThemes().stream().anyMatch(t -> DARK_THEME.equals(t.getId())); + boolean hasLight = themeEngine.getThemes().stream().anyMatch(t -> LIGHT_THEME.equals(t.getId())); + + String originalTheme = themeEngine.getActiveTheme() == null + ? LIGHT_THEME + : themeEngine.getActiveTheme().getId(); + + CSSCorePolicy.setPerfEnabled(true); + CSSCorePolicy.reset(); + if (hasDark && hasLight) { + runThemeSwapBenchmark(themeEngine); + } else { + runApplyStylesBenchmark(themeEngine, window); + } + System.out.print(CSSCorePolicy.summary()); + CSSCorePolicy.setPerfEnabled(false); + + swap(themeEngine, originalTheme); + } + + private List createJavaProjectWithFiles(String name, int fileCount) throws Exception { + IProject p = ResourcesPlugin.getWorkspace().getRoot().getProject(name); + if (p.exists()) { + p.delete(true, true, null); + } + p.create(null); + p.open(null); + IProjectDescription desc = p.getDescription(); + desc.setNatureIds(new String[] { JavaCore.NATURE_ID }); + p.setDescription(desc, null); + + IFolder src = p.getFolder("src"); + src.create(true, true, null); + + IJavaProject jp = JavaCore.create(p); + IClasspathEntry[] cp = new IClasspathEntry[] { + JavaCore.newSourceEntry(src.getFullPath()), + JavaRuntime.getDefaultJREContainerEntry() }; + jp.setRawClasspath(cp, p.getFolder("bin").getFullPath(), null); + + List files = new ArrayList<>(fileCount); + for (int i = 0; i < fileCount; i++) { + IFile file = src.getFile("PerfClass" + i + ".java"); + String content = """ + public class PerfClass%d { + private int counter; + public void method1() { + for (int i = 0; i < 10; i++) { + counter++; + } + } + public String greet(String name) { + return "hello " + name; + } + public int square(int x) { + return x * x; + } + } + """.formatted(i); + file.create(new ByteArrayInputStream(content.getBytes(StandardCharsets.UTF_8)), true, null); + files.add(file); + } + project = p; + return files; + } + + private void runThemeSwapBenchmark(IThemeEngine themeEngine) { + for (int i = 0; i < WARMUP_ROUNDS; i++) { + swap(themeEngine, i % 2 == 0 ? DARK_THEME : LIGHT_THEME); + } + long[] samples = new long[MEASURE_ROUNDS]; + for (int i = 0; i < MEASURE_ROUNDS; i++) { + String target = i % 2 == 0 ? DARK_THEME : LIGHT_THEME; + long t0 = System.nanoTime(); + swap(themeEngine, target); + samples[i] = System.nanoTime() - t0; + } + report("theme swap dark<->light (real workbench)", samples); + } + + private void runApplyStylesBenchmark(IThemeEngine themeEngine, IWorkbenchWindow window) { + System.out.println("[CSS-PERF] dark/light themes not available; falling back to applyStyles benchmark"); + for (int i = 0; i < WARMUP_ROUNDS; i++) { + themeEngine.applyStyles(window.getShell(), true); + processEvents(); + } + long[] samples = new long[MEASURE_ROUNDS]; + for (int i = 0; i < MEASURE_ROUNDS; i++) { + long t0 = System.nanoTime(); + themeEngine.applyStyles(window.getShell(), true); + processEvents(); + samples[i] = System.nanoTime() - t0; + } + report("applyStyles full workbench shell", samples); + } + + private void swap(IThemeEngine engine, String themeId) { + engine.setTheme(themeId, false); + processEvents(); + } + + private void pumpEvents(Display display, long millis) { + long endAt = System.currentTimeMillis() + millis; + while (System.currentTimeMillis() < endAt) { + if (!display.readAndDispatch()) { + try { + Thread.sleep(10); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + } + } + } + + private void report(String label, long[] ns) { + long[] sorted = ns.clone(); + Arrays.sort(sorted); + long min = sorted[0]; + long median = sorted[sorted.length / 2]; + long p95 = sorted[(int) Math.min(sorted.length - 1, sorted.length * 0.95)]; + long max = sorted[sorted.length - 1]; + long sum = 0; + for (long v : ns) { + sum += v; + } + double meanMs = sum / 1_000_000.0 / ns.length; + System.out.printf( + "[CSS-PERF] %s rounds=%d min=%.2fms median=%.2fms p95=%.2fms max=%.2fms mean=%.2fms%n", + label, ns.length, + min / 1e6, median / 1e6, p95 / 1e6, max / 1e6, meanMs); + } +} diff --git a/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java new file mode 100644 index 00000000000..bced017b851 --- /dev/null +++ b/tests/org.eclipse.ui.tests.performance/src/org/eclipse/ui/tests/performance/parts/CssStressViewPart.java @@ -0,0 +1,209 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel and others. + * + * 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.tests.performance.parts; + +import org.eclipse.e4.ui.css.swt.dom.WidgetElement; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.CCombo; +import org.eclipse.swt.custom.CLabel; +import org.eclipse.swt.custom.ScrolledComposite; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Combo; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.DateTime; +import org.eclipse.swt.widgets.ExpandBar; +import org.eclipse.swt.widgets.ExpandItem; +import org.eclipse.swt.widgets.Group; +import org.eclipse.swt.widgets.Label; +import org.eclipse.swt.widgets.Link; +import org.eclipse.swt.widgets.List; +import org.eclipse.swt.widgets.ProgressBar; +import org.eclipse.swt.widgets.Scale; +import org.eclipse.swt.widgets.Spinner; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableColumn; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; +import org.eclipse.swt.widgets.ToolBar; +import org.eclipse.swt.widgets.ToolItem; +import org.eclipse.swt.widgets.Tree; +import org.eclipse.swt.widgets.TreeItem; +import org.eclipse.ui.part.ViewPart; + +/** + * View used by CSS theme-swap performance tests. Builds a large, varied widget + * tree so the CSS engine has meaningful work to do when a theme is applied. + */ +public class CssStressViewPart extends ViewPart { + + public static final String ID = "org.eclipse.ui.tests.performance.cssStressView"; + + private static final int WIDGET_COUNT = 4000; + private static final int GROUP_SIZE = 40; + + private Composite root; + + @Override + public void createPartControl(Composite parent) { + ScrolledComposite scroll = new ScrolledComposite(parent, SWT.V_SCROLL | SWT.H_SCROLL); + root = new Composite(scroll, SWT.NONE); + root.setLayout(new GridLayout(8, true)); + buildTree(root, WIDGET_COUNT); + scroll.setContent(root); + scroll.setExpandHorizontal(true); + scroll.setExpandVertical(true); + scroll.setMinSize(root.computeSize(SWT.DEFAULT, SWT.DEFAULT)); + } + + private void buildTree(Composite parent, int target) { + int remaining = target; + int groupIndex = 0; + while (remaining > 0) { + Group group = new Group(parent, SWT.NONE); + group.setText("Group " + groupIndex); + group.setLayout(new GridLayout(4, false)); + int n = Math.min(GROUP_SIZE, remaining); + for (int i = 0; i < n; i++) { + addWidget(group, (groupIndex * GROUP_SIZE + i) % 14); + } + remaining -= n; + groupIndex++; + } + } + + private void addWidget(Composite parent, int kind) { + switch (kind) { + case 0: + Label l = new Label(parent, SWT.NONE); + l.setText("label-" + kind); + if (kind % 7 == 0) { + WidgetElement.setCSSClass(l, "hot"); + } + break; + case 1: + Button push = new Button(parent, SWT.PUSH); + push.setText("push"); + break; + case 2: + Button check = new Button(parent, SWT.CHECK); + check.setText("check"); + break; + case 3: + Button radio = new Button(parent, SWT.RADIO); + radio.setText("radio"); + break; + case 4: + Text text = new Text(parent, SWT.BORDER); + text.setText("text"); + break; + case 5: + Combo combo = new Combo(parent, SWT.READ_ONLY); + combo.setItems("a", "b", "c"); + combo.select(0); + break; + case 6: + Spinner spinner = new Spinner(parent, SWT.BORDER); + spinner.setSelection(5); + break; + case 7: + List list = new List(parent, SWT.BORDER | SWT.SINGLE); + list.setItems("item1", "item2", "item3"); + GridData ld = new GridData(); + ld.heightHint = 40; + list.setLayoutData(ld); + break; + case 8: + Link link = new Link(parent, SWT.NONE); + link.setText("linked"); + break; + case 9: + ProgressBar pb = new ProgressBar(parent, SWT.HORIZONTAL); + pb.setSelection(40); + break; + case 10: + CLabel clabel = new CLabel(parent, SWT.NONE); + clabel.setText("clabel"); + break; + case 11: + CCombo ccombo = new CCombo(parent, SWT.BORDER); + ccombo.setItems(new String[] { "x", "y", "z" }); + ccombo.select(0); + break; + case 12: + Scale scale = new Scale(parent, SWT.HORIZONTAL); + scale.setSelection(30); + break; + case 13: + DateTime dt = new DateTime(parent, SWT.DATE | SWT.SHORT); + // no extra setup + if (dt == null) { + throw new IllegalStateException(); + } + break; + default: + break; + } + + if (kind == 4) { + // once per group cycle, add a small Tree / Table / ExpandBar / ToolBar so + // the engine sees container widgets too + addContainerWidgets(parent); + } + } + + private void addContainerWidgets(Composite parent) { + ToolBar tb = new ToolBar(parent, SWT.FLAT); + new ToolItem(tb, SWT.PUSH).setText("a"); + new ToolItem(tb, SWT.SEPARATOR); + new ToolItem(tb, SWT.PUSH).setText("b"); + + Tree tree = new Tree(parent, SWT.BORDER); + GridData td = new GridData(); + td.heightHint = 50; + tree.setLayoutData(td); + TreeItem root = new TreeItem(tree, SWT.NONE); + root.setText("root"); + new TreeItem(root, SWT.NONE).setText("child-1"); + new TreeItem(root, SWT.NONE).setText("child-2"); + + Table table = new Table(parent, SWT.BORDER | SWT.FULL_SELECTION); + table.setHeaderVisible(true); + TableColumn c1 = new TableColumn(table, SWT.LEFT); + c1.setText("name"); + c1.setWidth(60); + TableColumn c2 = new TableColumn(table, SWT.LEFT); + c2.setText("value"); + c2.setWidth(60); + TableItem ti = new TableItem(table, SWT.NONE); + ti.setText(new String[] { "k1", "v1" }); + GridData tabd = new GridData(); + tabd.heightHint = 50; + table.setLayoutData(tabd); + + ExpandBar bar = new ExpandBar(parent, SWT.NONE); + ExpandItem ei = new ExpandItem(bar, SWT.NONE); + ei.setText("section"); + Composite inner = new Composite(bar, SWT.NONE); + inner.setLayout(new GridLayout(1, false)); + new Label(inner, SWT.NONE).setText("inner"); + ei.setControl(inner); + ei.setHeight(40); + } + + @Override + public void setFocus() { + if (root != null && !root.isDisposed()) { + root.setFocus(); + } + } +} From 36a261693cb7b402a97e384017a559ab4f347008 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Thu, 21 May 2026 22:22:21 +0200 Subject: [PATCH 19/19] docs: document CSS engine performance baseline and benchmarks in CSS-rework.md Contributes to #3980 --- CSS-rework.md | 478 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 478 insertions(+) create mode 100644 CSS-rework.md diff --git a/CSS-rework.md b/CSS-rework.md new file mode 100644 index 00000000000..3f9e87d231b --- /dev/null +++ b/CSS-rework.md @@ -0,0 +1,478 @@ +# CSS Engine Rework Plan + +## Status (2026-05-07) + +- **Phase 0 — mechanical cleanups: all merged.** + - `BootstrapTheme3x` removal (#3975). + - `IOException` removal from `CSSEngine` String overloads (#3976). + - Dead `SACConstants` parser entries removed (#3977). + - Unused CSS serializer classes and dead color converter config (#3978). +- **Phase 1 — test safety net: all merged.** + - Phase 1 selector matching tests (#3970). + - Phase 2 parser round-trip tests (#3974). + - Padding gap-fill (#3979). Remaining handler gaps from the Phase 3 + audit in `css-testing.md` are scoped down; revisit only on regression. + - CSS selector integration tests for tab selection, `.active` class, + and preference pseudo (#3983, in review): pins three engine + behaviours that the matcher / parser unit tests do not exercise + end-to-end. +- **Phase 2 + Phase 3 step 1 — in flight on a single branch + (`vogella/css-engine-rework`).** Seven commits stacked, intended to + be pushed out as individual PRs once stable. + - `AbstractCSSEngine` merged into `CSSEngineImpl`. + - `AbstractCSSSWTEngineImpl` merged into `CSSSWTEngineImpl`. + - `ICSSPropertyHandler2` and `ICSSPropertyHandler2Delegate` folded + into `ICSSPropertyHandler` via Java 21 default methods. + - Unused `PropertyHelper` and its self-test deleted. + - The vendored 3,205-line `URI` copy replaced with a Require-Bundle + on `org.eclipse.emf.common`. + - Internal CSS Selector AST + matcher introduced (Phase 3 step 1 + foundation). + - **Phase 3 step 1 wiring (this commit):** SAC→`Selectors` + translator added at the parser-output boundary + (`CSSDocumentHandlerImpl.startSelector`); `CSSEngine.matches` and + `parseSelectors` switched to the internal `Selectors.Selector` + type; `CSSEngineImpl.matches` delegates to `SelectorMatcher`; + `applyConditionalPseudoStyle` rewritten to walk the internal AST; + parser configured with Batik's stock `DefaultSelectorFactory` / + `DefaultConditionFactory`; 23 vendored `impl/sac/*` selector and + condition wrappers deleted; the dead + `ExtendedDocumentCSS.queryConditionSelector` / + `querySelector` (and the `SAC_*_CONDITION` int constants behind + them) removed; `CSSEngineTest` and `SelectorTest` rewritten on + the internal AST. `CSSDocumentHandlerImpl`, + `DocumentHandlerFactoryImpl`, and `SACParserFactoryImpl` are the + only `impl/sac/*` classes still standing; they go in step 2. + Net ~−4,750 LOC plus +711 LOC of Selector AST scaffolding plus + +160 LOC of translator; all existing tests pass. + Phase 2 leftovers (helper consolidation, SAC-bound Abstract-class + merges, factory layer collapse) deferred: helper consolidation + is high blast radius, the SAC-bound work is Phase 4 collateral. +- **Phase 3 step 1 — drop SAC types from the engine:** complete on + `css-engine-rework`. Engine has zero SAC selector/condition types + in its public API or matcher; SAC selectors only appear at the + parser boundary inside `CSSDocumentHandlerImpl.startSelector`, + where the translator hands them to the internal AST. +- **Phase 3 step 2 — replace Batik with hand-written tokenizer:** not + started. Plan agreed: two commits (parser added but not wired, + then cut over + delete Batik and SAC plumbing). No fallback flag, + no parallel-parser smoke harness; existing test suite is the + gate. Phase 0 test gap-fill (`!important` parse-and-pin, + `:hover`/`:focus`/`:active` parser tolerance, string escapes in + attribute selectors, etc.) considered and skipped as not critical. +- **Phase 4 — DOM mirror replacement:** not started. +- **Phases 5–6:** not started. + +Goal: trim the e4 CSS stack (`org.eclipse.e4.ui.css.core`, +`org.eclipse.e4.ui.css.swt`, `org.eclipse.e4.ui.css.swt.theme`, +~30k LOC across 263 classes) to roughly two thirds of its current size +while keeping shipped Eclipse themes working. The shipped CSS subset is +small (see `css-testing.md`), and most of the bulk is dead-API plumbing, +parser wrapping, and one-class-per-property handler files. + +## Current state + +- Three bundles, ~30k LOC, 263 classes. Bundle `css.core` is internal + (`Export-Package: ...;x-friends:=` and `x-internal:=true` everywhere), + so signatures can change. +- Parser today: Apache Batik 1.9.x (2017) accessed through W3C SAC + (`org.w3c.css.sac` 1.3.0, last revised ~2003). 26 classes under + `impl/sac/*` plus 3 façade classes under `core/sac/*` adapt SAC into + the engine. ~40 files across the three bundles plus tests import SAC + types directly. +- A second mirror layer (`impl/dom/*`, ~32 classes) reimplements W3C + DOM-CSS types (`CSSStyleSheet`, `CSSStyleRule`, `CSSValue`, etc.) for + internal use only. Nothing outside the engine creates or mutates those + objects. +- A registry-driven handler dispatch (`RegistryCSSPropertyHandlerProvider`, + ~1.1k LOC) reads two extension points (`elementProvider`, + `propertyHandler`) that no in-tree contributor outside `css.swt` uses. +- 74 property-handler classes, most of them 40 to 50 line wrappers around + one setter call (`CSSPropertyMaximizeVisibleSWTHandler`, etc.). +- Phase 1 of `css-testing.md` is merged: `CSSEngineTest` covers + selector matching. Phases 2 (parser round-trip) and 3 (property + handlers) are the safety net for the rest of this plan. + +## Guiding constraints + +- Preserve the high-level engine contract used by callers: + `CSSEngine.applyStyles`, `matches`, `parseStyleSheet`. Internal types + (`impl/dom/*`, `impl/sac/*`, registry providers) are fair game. +- Limit semantic scope to what shipped Eclipse stylesheets use: type / + class / id selectors, `@import`, attribute selectors `=` and `~=`, + child / descendant combinators, `:selected` and `:disabled`. Out: + `@media`, `@font-face`, `:hover` / `:focus` / `:active`, `!important`. +- No regressions in shipped themes (`platform`, `dark`, + `org.eclipse.ui.themes` contributions). Each phase ships behind the + Phase 1 + 2 + 3 tests as the gate. +- Each phase ships as one (or a small number of) PR. No mega-PRs. + +## Phases + +The phases are roughly in landing order. Earlier phases unblock later +ones; risk grows as we move down. + +### Phase 0 — small mechanical cleanups (independent) + +Ship now, no design needed. + +- Drop `throws IOException` from `CSSEngine.parseSelectors(String)`, + `parsePropertyValue(String)`, and any other engine method whose String + overload only throws because of a `StringReader` hop. The signature + comes from SAC's `Parser.parseSelectors(InputSource)` and is a checked + exception that can never fire. Bundle is internal; callers fix in one + pass. +- Delete dead parser entries from `SACConstants` (`SteadyState`, the + unused Flute CSS3 variant, etc.). +- ~~Drop the `BootstrapTheme3x` shim and any other Eclipse 3.x compatibility + glue in `css.swt.theme`. The 3.x bridge runtime is no longer the + expected target.~~ Done (#3975). +- Audit and delete unused converter / serializer classes under `css.swt` + (the `converter` and `serializer` subpackages each have a couple of + entries with no in-tree caller). + +Effort: ~1 day. ~500 LOC removed. Low risk. + +### Phase 1 — finish the test safety net (from css-testing.md) + +- Phase 2: `StyleSheetStructureTest` for parser round-trip. +- Phase 3a: property-handler audit (no PR, just the gap list). +- Phase 3b: fill in handler tests using + `EclipsePreferencesHandlerTest` / `CSSSWTTestCase` patterns. + +Effort: 3 to 4 days, 2 PRs. ~1,000 LOC added. Without this in place the +later phases are flying blind. + +### Phase 2 — flatten engine and helper hierarchies (low risk, no API impact) + +Pure refactor. No behaviour change. Each bullet is its own PR. + +- Merge `AbstractCSSEngine` (1,113 LOC) and `CSSEngineImpl` (~95 LOC) + into a single class. Same for `AbstractCSSSWTEngineImpl` / + `CSSSWTEngineImpl`. These hierarchies have one concrete subclass each. +- Collapse `Abstract*Selector` and `Abstract*Condition` classes whose + only subclass is the concrete `*Impl`. Several of these go away + naturally during the SAC swap; do whichever ones can be done now + without touching SAC, leave the rest for Phase 4. +- Collapse the factory layers (`CSSSelectorFactoryImpl`, + `CSSConditionFactoryImpl`, `DocumentHandlerFactoryImpl`) where there + is only one concrete factory. +- Consolidate the 9 SWT helpers into 3 cohesive ones: `SwtCssColors` + (current `CSSSWTColorHelper`), `SwtCssFonts`, `SwtCssWidgets`. Delete + `PropertyHelper`, `CSSSWTHelpers`, `SWTStyleHelpers` after their + callers move. +- Merge `ICSSPropertyHandler2` and `ICSSPropertyHandler2Delegate` into + `ICSSPropertyHandler` using Java 21 default methods. The bundle uses + `BREE: JavaSE-21`; this is mechanical. + +Effort: 4 to 5 days, 4 to 5 PRs. ~2,000 LOC removed, ~25 classes +removed. Low risk. + +### Phase 3 — drop SAC, then drop Batik + +The original plan named "swap Batik" and "drop SAC" as one phase. They +are not the same change: empirically, Eclipse depends on Batik for +exactly one runtime artifact (`org.apache.batik.css.parser.Parser`, +loaded reflectively); everything else under our `impl/sac/*` is a +22-year-old vendored copy of Batik selector wrappers. So Phase 3 +splits cleanly into two steps that are individually shippable. + +**Step 1 — drop SAC types from the engine, keep Batik.** + +The engine API today exposes SAC types +(`org.w3c.css.sac.SelectorList parseSelectors(...)`, +`boolean matches(Selector, Object, String)`), and the 26 vendored +classes under `impl/sac/*` implement SAC interfaces +(`ExtendedSelector`, `ExtendedCondition`) extended with +`match(Element, pseudo)` and `getSpecificity()`. Replace this with: + +- An internal `Selector` AST as a sealed interface plus records: + `TypeSelector`, `ClassSelector`, `IdSelector`, `AttributeSelector`, + `PseudoClassSelector`, `CompoundSelector`, `DescendantSelector`, + `ChildSelector`, `SelectorList`. Engine-internal, no SAC. +- A `SelectorMatcher` service with `boolean matches(Selector, Element, + String pseudo)` that walks the new AST. +- A small translator that consumes the SAC selector trees the Batik + parser still produces and emits the new internal AST. Lives at the + parser-output boundary; called once per stylesheet load. Uses + Batik's stock SAC `DefaultSelectorFactory` / + `DefaultConditionFactory` instead of our vendored copies. +- `CSSEngine.matches` and `parseSelectors` change to return / accept + the internal `Selector` type. Internal API only (`x-friends`); no + external API break. +- Delete the 26 vendored classes under `impl/sac/*` and the 3 façade + classes under `core/sac/*` once they are no longer reachable. + +The Phase 1 selector-matching tests (`CSSEngineTest`, merged via +#3970) already assert through `engine.matches(...)`. They get rewritten +to drop the SAC `Selector` import and use the new internal type +directly. The Phase 2 round-trip tests do not touch selectors and stay +unchanged. + +Effort: 4 to 5 days, 1 PR. ~600 LOC added (AST + matcher + translator) +and ~2,000 LOC removed (the 26 vendored wrappers and the 3 façades). +Net ~−1,400 LOC. Medium risk: specificity calculation must match +current behaviour exactly so cascade ordering does not shift. + +**Step 2 — replace Batik with a hand-written CSS3 tokenizer.** + +After step 1 the only Batik touch-point is the reflectively-loaded +`org.apache.batik.css.parser.Parser` class plus a thin SAC layer +around it (3 remaining `impl/sac/*` parser-plumbing classes, 6 +`core/sac/*` façades). Replace all of that with a small hand-written +tokenizer + recursive-descent parser scoped to the CSS subset Eclipse +and downstream RCP applications use. That subset is not as small as +`css-testing.md` originally suggested: in addition to type / class / +id / `:pseudo` / attribute / child / descendant selectors, the engine +machinery actively supports `:focus` (Control), `:active` (Shell), +arbitrary pseudo-classes through `isPseudoInstanceOf`, and parses +(without applying) `!important`, `@media`, `@font-face`. The new +tokenizer must accept all of this even though no in-repo stylesheet +uses the dormant features. + +Rough sizing: + +| Piece | LOC | +|---|---| +| Tokenizer | 250–350 | +| Selector parser | 150–250 | +| Declaration / value parser | 150–250 | +| AST records | 100–200 | +| Specificity calculation | ~30 | +| **Total** | **~700–1,100** | + +Two commits, each shippable as its own PR. + +1. **Add the new parser, not yet wired.** New package + `impl/parser/` with the tokenizer, selector parser (emits + `Selectors.SelectorList` directly — no SAC, no translator), + declaration/value parser (emits the existing `impl/dom/*` types + so Phase 4 can replace those types in a focused follow-up), + stylesheet parser (emits `CSSStyleSheetImpl` / + `CSSStyleRuleImpl` / `CSSImportRuleImpl`; parses `@media` / + `@font-face` / `!important` and discards them), plus a + `CssParseException` replacing `org.w3c.css.sac.CSSException`. + Inline unit tests for tokenizer and parser. Pure addition; + reviewable in isolation. + +2. **Cut over and delete.** Wire the new parser into + `CSSEngineImpl.makeCSSParser`, drop + `Require-Bundle: org.apache.batik.css` and the SAC + `Import-Package`, delete the 3 remaining `impl/sac/*` classes, + the 6 `core/sac/*` façades, `SacTranslator`, `SACConstants`, + `core/dom/parsers/CSSParser` + `CSSParserFactory` + + `ICSSParserFactory`, `impl/dom/parsers/AbstractCSSParser` + + `CSSParserFactoryImpl`, and the four `InputSource` overloads on + `CSSEngine` (`Reader` / `InputStream` overloads remain). + Migrate the few non-engine callers off `InputSource`: + `ThemeEngine`, `ParserTestUtil`, `ImportTest`, the css.swt + margin/padding handlers' `CSSException` references. Bump + `org.eclipse.e4.ui.css.core` Bundle-Version (no API baseline + failure since the bundle's exports are all `x-internal` / + `x-friends`). + +No fallback: the legacy parser is removed in commit 2 in one go, +not behind a system property. No parallel-parser smoke harness: +the existing test suite (CSSEngineTest, SelectorMatcherTest, +StyleSheetStructureTest, CascadeTest, ValueTest, ImportTest, +MediaRulesTest, FontFaceRulesTest, InheritTest, ViewCSSTest) is +the gate. The Phase 0 test gap-fill (`!important` parse-and-pin, +`:hover`/`:focus`/`:active` parser tolerance, string escapes in +attribute selectors, expanded `url()` forms, `rgba`, +trailing-semicolon-optional, etc.) was considered and skipped as +not critical: if the new parser is wrong about any of these, the +shipped themes will fail visibly rather than silently. + +Effort: 3 to 5 days, 2 PRs. Net ~+900 / −3,400 across the two +commits, ~−2,500 net. + +Combined Phase 3 (step 1 + step 2): 2 PRs landed for step 1, 2 +PRs for step 2, net ~−3,900 LOC, drops one external runtime +dependency, leaves zero SAC types in the codebase. + +### Phase 4 — replace the W3C DOM mirror with internal POJOs (re-ordered, was Phase 3) + +`impl/dom/*` (~32 classes, 3,500 to 4,500 LOC) implements `CSSStyleSheet`, +`CSSStyleRule`, `CSSImportRule`, `CSSValueList`, `RGBColor`, `Measure`, +etc., for an SWT consumer that never asks for DOM compliance. After +Phase 3 the new `BatikStyleSheetParser` is the only producer of these +types and the SAC layer is gone, so the mirror can be replaced with +plain rule, selector, and value records: + +```java +record StyleSheet(List rules) {} +sealed interface Rule permits StyleRule, ImportRule {} +record StyleRule(List selectors, Map declarations) {} +record ImportRule(URI href) {} +sealed interface CssValue permits ColorValue, NumberValue, ListValue, KeywordValue, StringValue {} +``` + +Phase 3's parser now emits the POJOs instead of the wrapper classes; +engine and property handlers shift to read the new types directly. The +Phase 1 + Phase 2 tests in `css-testing.md` need to be rewritten on +the new types as part of this phase, since they currently assert +against the W3C DOM-CSS interfaces. + +Risk: medium. Cascade ordering, specificity, and `@import` resolution +all live here. Drop them carefully. + +Effort: 8 to 12 days, 1 large PR or 2 medium PRs. ~3,500 LOC removed, +20 to 25 classes removed. + +### Phase 5 — collapse trivial property-handler classes + +Today: `RegistryCSSPropertyHandlerProvider` reads the +`org.eclipse.e4.ui.css.core.propertyHandler` extension point, builds a +map keyed by element-class + property name, and dispatches into one of +74 handler classes. Most handlers are stateless one-liners — 15 to 20 +of them are near-identical boolean / int / color setters wrapped in +boilerplate. + +The registry-based dispatch stays. Clients override our handlers by +contributing to the same extension point, so we cannot bypass it for +our own handlers without breaking the override contract. The +consolidation happens inside the handler classes: + +- One `GenericBooleanSWTHandler` registered in `plugin.xml` for every + boolean SWT property (`maximize-visible`, `minimize-visible`, + `mru-visible`, ...). Its `applyCSSProperty(element, property, value, + ...)` dispatches on `property` to a small lookup map of + `BiConsumer`. +- Same shape for `GenericIntSWTHandler`, + `GenericColorSWTHandler`, etc., where the property-to-setter + mapping is regular. +- The non-trivial appliers (margins, paddings, preferences, + CTabFolder visual rendering) keep their dedicated classes; their + logic does not collapse cleanly. + +Plugin.xml contribution shape stays one entry per (element-class, +property): the registry still finds external overrides at the same +granularity. We just gain one bit of property dispatch inside the +shared handler. Net effect: ~30 wrapper classes deleted, ~3 to 5 +generic handlers added, no schema or contract change. + +Out of scope: removing or deprecating the `propertyHandler` / +`elementProvider` extension points, or `RegistryCSSPropertyHandlerProvider` +itself. They stay public and functional for downstream RCP products +that contribute custom handlers, including overrides of the new +generic handlers. This caps the LOC delta but keeps external +contracts intact. + +Effort: 5 to 7 days across 2 to 3 PRs. ~1,500 to 1,800 LOC removed, +~30 wrapper classes removed. Medium risk; the override path is +exercised by external contributors, so the new generic handlers must +not change observable behaviour for any single (element, property) +pair. + +### Phase 6 — merge `css.swt.theme` into `css.swt` + +`css.swt.theme` is 7 classes / ~1,100 LOC of theme manager wiring. It +does not justify its own bundle, MANIFEST, feature.xml entry, p2 IU, and +test bundle. Inline as an internal package of `css.swt`. + +This is logistics-heavy (feature.xml, target platform updates, +downstream build files reference the bundle by name) more than +code-heavy. Do it last, when no other phase is touching the bundle +boundary. + +Effort: 2 to 3 days. ~200 LOC net. Medium risk (build-system blast +radius). + +## Order of work + +| Order | Phase | LOC delta | Risk | +|---|---|---|---| +| 1 | Phase 0 — mechanical cleanups | ~-500 | Low | +| 2 | Phase 1 — finish test safety net | ~+1,000 | Low | +| 3 | Phase 2 — flatten hierarchies and helpers | ~-2,000 | Low | +| 4 | Phase 3 step 1 — drop SAC types, keep Batik | ~-1,400 | Medium | +| 5 | Phase 3 step 2 — replace Batik with hand-written tokenizer | ~-2,500 | Medium | +| 6 | Phase 4 — replace DOM mirror with POJOs | ~-3,500 | Medium | +| 7 | Phase 5 — collapse trivial property-handler classes | ~-1,700 | Medium | +| 8 | Phase 6 — merge `css.swt.theme` into `css.swt` | ~-200 | Medium | +| **Total** | | **~-10,800** | | + +Phases 3 and 4 swapped relative to the original plan: the parser is the +only producer of the W3C DOM mirror types, so replacing the parser +first leaves a single, well-scoped change to delete the mirror in +Phase 4. The original "DOM-mirror first, parser second" ordering would +have required a temporary W3C → POJO conversion layer to keep the SAC +parser feeding into a new model, plus rewriting the Phase 2 round-trip +tests twice (once when the model changes, again when the parser does). + +Roughly a third of the current LOC, in line with the upper end of the +analyses in `temp1`, `temp2`, `temp3`. + +## Performance & Optimization Benchmarks + +Following the integration of Phase 3 Step 1 (internal AST and matcher), profiling identified a performance regression during CSS theme swaps. A detailed optimization effort was carried out to establish a new performance baseline. + +### Bottlenecks Identified +1. **Dynamic Parent Resolution**: `SelectorMatcher` dynamically traversed parent nodes by invoking `element.getParentNode()`. In SWT, this triggers costly Map lookups and potential DOM element adapter instantiations. +2. **Traversal Overlap**: Redundant styling passes (`Shell.reskin(SWT.ALL)` followed immediately by recursive `CSSEngine.applyStyles`) caused elements to be styled multiple times in a single theme swap. + +### Optimizations Implemented +1. **Pre-computed Ancestor Hierarchy**: The matching path was overloaded to accept a pre-computed array of ancestor elements, avoiding dynamic `getParentNode()` lookups. +2. **Styling Sessions**: A thread-local `styledElements` session was introduced to track and prevent duplicate styling of widgets within a single theme swap operation. + +### Benchmark Results +The optimizations were verified using the stress test `CssThemeSwapPerformanceTest` (which styles a workbench containing 4,000+ SWT widgets and 20 Java editors). + +| Metric | Unoptimized Baseline (Regression) | Optimized Implementation | Improvement | +| :--- | :--- | :--- | :--- | +| **Median Time** | 2091.30 ms | 1322.55 ms (up to 1136.51 ms) | ~36.8% (up to ~45.6%) | +| **Mean Time** | 2119.23 ms | 1339.35 ms | ~36.8% | +| **Minimum Time** | 1890.94 ms | 1129.77 ms | ~40.3% | +| **Maximum Time** | 2425.58 ms | 2050.99 ms | ~15.4% | + +## Risks worth calling out + +- **Test coverage is thin.** Phase 1 (the test net) is non-negotiable. + Skipping it roughly doubles iteration counts on every later phase. +- **Theme regressions.** Add a smoke test that parses every `.css` under + `bundles/**/css/` with both old and new parser during Phase 4 and + compares selector text + declaration counts. Fail on divergence. +- **Closed-source RCP consumers.** Custom `CSSEngine` subclasses likely + have external users; the `propertyHandler` and `elementProvider` + extension points might. Both extension points stay public (see Phase + 5 — only our in-tree contributions move to a static dispatch). Phase + 6 (`css.swt.theme` inlining) is the only phase that removes a public + surface; ship it with a deprecation cycle. +- **Pseudo-element semantics.** The current SAC matcher has a quirk: + `CSSPseudoClassConditionImpl.match` returns + `!isStaticPseudoInstance(value)` when `pseudoE == null`. Lock this in + a Phase 1 test before Phase 4 starts, or define the new behaviour + explicitly and migrate the few `ElementAdapter` subclasses that depend + on the static-pseudo registration. +- **Specificity calculation.** Cascade order depends on it. Phase 2 + test additions must include a specificity case before Phase 3 lands. +- **Batik tightening.** Going from a SAC façade to direct Batik usage + couples us more to Batik. Acceptable in exchange for ~2k LOC removed + today; revisit if Batik itself ever needs to be replaced. + +## Out of scope + +- Replacing Batik with ph-css or a hand-written tokenizer. Defer until + after Phase 5 if at all. Mixing the parser swap with a dependency swap + is what makes /temp3 estimate Phase 4 at "high risk". +- Adding new CSS features (`:hover`, `!important`, `@media`). +- Deprecating or removing the `elementProvider` and `propertyHandler` + extension points. They stay public so downstream RCP products can + keep contributing custom handlers. Phase 5 only collapses our own + in-tree contributions; the registry-based dispatch path stays alive + for external contributors. +- Replacing the engine for non-SWT clients. The engine is SWT-only in + practice; treat that as an invariant. + +## Low-priority follow-ups + +- **Replace `CSSSWTTestCase` inheritance with a JUnit 5 extension.** + About 25 widget tests in `tests/org.eclipse.e4.ui.tests.css.swt` + currently extend `CSSSWTTestCase` to inherit a `display` field, an + engine factory, and a tearDown that disposes shells. Convert it to a + `BeforeEachCallback`/`AfterEachCallback` extension registered via + `@RegisterExtension CssSwtEngine css = new CssSwtEngine()`, which + drops the protected mutable state, lets tests use multiple engines + or stylesheets per test, and frees the test classes to extend other + bases. Defer until after Phase 3 of `css-testing.md` lands so this + migration does not merge-conflict against the gap-fill PRs.