From 803ae838839b3c40597118d7efab06550fc90998 Mon Sep 17 00:00:00 2001 From: gregdyke Date: Thu, 5 Dec 2024 12:00:41 +0000 Subject: [PATCH] Allow semantic highlighting to unset bold and italic (#1153) Semantic highlighting needs to be able to override the styles set by TM4E. Simply not setting the styles in TM4E would introduce flickering in the delay waiting for semantic highlighting. If unsetting of bold and/or italic is not desired, disable using semanticHighlightReconciler.ignoreBoldNormal and semanticHighlightReconciler.ignoreItalicNormal ---- When applying semantic highlighting, use TextPresentation#mergeStyleRanges to create new style ranges and merge as "usual". Then traverse resulting style ranges and unset bold/italic where it is not set in semantic highlighting. The double traversal algorithm is simplified because after calling TextPresentation#mergeStyleRanges we are guaranteed that each style range from semantic highlighting has exact overlap with 1 or more style ranges in the textPresentation. ---- Because textmate uses css, style {font-weight:"normal"} should be expected to modify the font-weight, whereas {font-weight:"inherit"} should not. However, TM4E parses the style into a an SWT StyleRange which prevents the differentiation between "normal"=unset bold if it is set and "inherit"=don't unset bold if it is set. A more long term solution would use the css to determine whether an unset bold bit is overriding or not. For the medium term we simply say "semantic highlighting has ownership of bold/italic and the style sheet must set them explicitly" --- .../semanticTokens/StyleRangeMergerTest.java | 184 ++++++++++++++++++ .../SemanticHighlightReconcilerStrategy.java | 10 +- .../semanticTokens/StyleRangeMerger.java | 129 ++++++++++++ 3 files changed, 319 insertions(+), 4 deletions(-) create mode 100644 org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeMergerTest.java create mode 100644 org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/StyleRangeMerger.java diff --git a/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeMergerTest.java b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeMergerTest.java new file mode 100644 index 000000000..e7406942d --- /dev/null +++ b/org.eclipse.lsp4e.test/src/org/eclipse/lsp4e/test/semanticTokens/StyleRangeMergerTest.java @@ -0,0 +1,184 @@ +/******************************************************************************* + * Copyright (c) 2022 Avaloq Evolution AG. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lsp4e.test.semanticTokens; + +import static org.eclipse.lsp4e.test.semanticTokens.SemanticTokensTestUtil.GREEN; +import static org.eclipse.lsp4e.test.semanticTokens.SemanticTokensTestUtil.RED; +import static org.junit.Assert.assertEquals; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.jface.text.TextPresentation; +import org.eclipse.lsp4e.operations.semanticTokens.StyleRangeMerger; +import org.eclipse.lsp4e.operations.semanticTokens.StyleRangeHolder; +import org.eclipse.lsp4e.test.utils.AbstractTest; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; +import org.eclipse.swt.graphics.Color; +import org.junit.Test; + +public class StyleRangeMergerTest extends AbstractTest { + + @Test + public void testSemanticHighlightMergesWithExistingStyleRanges() { + // make 3 adjacent regions: ___AABBCC + int length = 2; + Region rangeA = new Region(2, length); + Region rangeB = new Region(rangeA.getOffset() + length, length); + Region rangeC = new Region(rangeB.getOffset() + length, length); + + StyleRange backGroundRangeAB = aStyleRange(span(rangeA, rangeB), null, GREEN, SWT.NORMAL); + StyleRange boldRedStyleRangeBC = aStyleRange(span(rangeB, rangeC), RED, null, SWT.BOLD); + + List resultingStyleRanges = mergeStyleRanges(new StyleRange[] { backGroundRangeAB }, + new StyleRange[] { boldRedStyleRangeBC }); + + assertEquals(aStyleRange(rangeA, null, GREEN, SWT.NORMAL), resultingStyleRanges.get(0)); + assertEquals(aStyleRange(rangeB, RED, GREEN, SWT.BOLD), resultingStyleRanges.get(1)); + assertEquals(aStyleRange(rangeC, RED, null, SWT.BOLD), resultingStyleRanges.get(2)); + + } + + @Test + public void testSemanticHighlightUnBoldsExistingStyleRanges() { + // make 4 adjacent regions: ___AABBCCDD + int length = 2; + Region rangeA = new Region(2, length); + Region rangeB = new Region(rangeA.getOffset() + length, length); + Region rangeC = new Region(rangeB.getOffset() + length, length); + Region rangeD = new Region(rangeC.getOffset() + length, length); + + StyleRange[] existingRanges = new StyleRange[] { // + aStyleRangeWithFontStyle(rangeA, SWT.BOLD), // + aStyleRangeWithFontStyle(rangeB, SWT.BOLD), // + aStyleRangeWithFontStyle(rangeC, SWT.BOLD), // + aStyleRangeWithFontStyle(rangeD, SWT.BOLD) // + }; + + StyleRange[] newRanges = new StyleRange[] { // + aStyleRangeWithFontStyle(rangeA, SWT.NORMAL), // + aStyleRangeWithFontStyle(rangeC, SWT.NORMAL), // + aStyleRangeWithFontStyle(rangeD, SWT.NORMAL), // + }; + + List resultingStyleRanges = mergeStyleRanges(existingRanges, newRanges); + + assertEquals(aStyleRangeWithFontStyle(rangeA, SWT.NORMAL), resultingStyleRanges.get(0)); + assertEquals(aStyleRangeWithFontStyle(rangeB, SWT.BOLD), resultingStyleRanges.get(1)); + assertEquals(aStyleRangeWithFontStyle(rangeC, SWT.NORMAL), resultingStyleRanges.get(2)); + assertEquals(aStyleRangeWithFontStyle(rangeD, SWT.NORMAL), resultingStyleRanges.get(3)); + } + + @Test + public void testSemanticHighlightMergesWithAndUnBoldsExistingStyleRanges() { + // make adjacent regions: AABBCCDDEEFFGGHHII + // existing style spans: XXXX VVYYYYWWZZ + // new style spans: LLMMMMMMMM NNNNNN + int length = 2; + Region rangeA = new Region(0, length); + Region rangeB = new Region(rangeA.getOffset() + length, length); + Region rangeC = new Region(rangeB.getOffset() + length, length); + Region rangeD = new Region(rangeC.getOffset() + length, length); + Region rangeE = new Region(rangeD.getOffset() + length, length); + Region rangeF = new Region(rangeE.getOffset() + length, length); + Region rangeG = new Region(rangeF.getOffset() + length, length); + Region rangeH = new Region(rangeG.getOffset() + length, length); + Region rangeI = new Region(rangeH.getOffset() + length, length); + + StyleRange[] existingRanges = new StyleRange[] { // + aStyleRange(span(rangeA, rangeB), GREEN, null, SWT.BOLD), // + aStyleRange(rangeD, null, null, SWT.BOLD), // + aStyleRange(span(rangeE, rangeF), null, GREEN, SWT.BOLD), // + aStyleRange(rangeG, null, GREEN, SWT.NORMAL), // + aStyleRange(rangeH, null, null, SWT.BOLD) // + }; + + StyleRange[] newRanges = new StyleRange[] { // + aStyleRange(rangeA, null, RED, SWT.BOLD), // + aStyleRange(span(rangeB, rangeE), RED, null, SWT.NORMAL), // + aStyleRange(span(rangeG, rangeI), null, null, SWT.NORMAL), // + }; + + List resultingStyleRanges = mergeStyleRanges(existingRanges, newRanges); + + assertEquals(aStyleRange(rangeA, GREEN, RED, SWT.BOLD), resultingStyleRanges.get(0)); + assertEquals(aStyleRange(rangeB, RED, null, SWT.NORMAL), resultingStyleRanges.get(1)); + assertEquals(aStyleRange(rangeC, RED, null, SWT.NORMAL), resultingStyleRanges.get(2)); + assertEquals(aStyleRange(rangeD, RED, null, SWT.NORMAL), resultingStyleRanges.get(3)); + assertEquals(aStyleRange(rangeE, RED, GREEN, SWT.NORMAL), resultingStyleRanges.get(4)); + assertEquals(aStyleRange(rangeF, null, GREEN, SWT.BOLD), resultingStyleRanges.get(5)); + assertEquals(aStyleRange(rangeG, null, GREEN, SWT.NORMAL), resultingStyleRanges.get(6)); + assertEquals(aStyleRange(rangeH, null, null, SWT.NORMAL), resultingStyleRanges.get(7)); + assertEquals(aStyleRange(rangeI, null, null, SWT.NORMAL), resultingStyleRanges.get(8)); + } + + @Test + public void testSemanticHighlightCombinesItalicAndBoldWithExistingStyleRanges() { + // make 3 adjacent regions: AABBCC + int length = 2; + Region rangeA = new Region(0, length); + Region rangeB = new Region(rangeA.getOffset() + length, length); + Region rangeC = new Region(rangeB.getOffset() + length, length); + Region rangeD = new Region(rangeC.getOffset() + length, length); + + // in these tests, bold gets overridden by "not-bold", but italic does not get + // overridden + StyleRange[] existingRanges = new StyleRange[] { // + aStyleRangeWithFontStyle(rangeA, SWT.BOLD | SWT.ITALIC), // + aStyleRangeWithFontStyle(rangeB, SWT.BOLD), // + aStyleRangeWithFontStyle(rangeC, SWT.BOLD), // + aStyleRangeWithFontStyle(rangeD, SWT.ITALIC), // + }; + + StyleRange[] newRanges = new StyleRange[] { // + aStyleRangeWithFontStyle(rangeA, SWT.NORMAL), // + aStyleRangeWithFontStyle(rangeB, SWT.ITALIC), // + aStyleRangeWithFontStyle(rangeC, SWT.BOLD | SWT.ITALIC), // + aStyleRangeWithFontStyle(rangeD, SWT.BOLD), // + }; + + List resultingStyleRanges = mergeStyleRanges(existingRanges, newRanges); + + assertEquals(aStyleRangeWithFontStyle(rangeA, SWT.ITALIC), resultingStyleRanges.get(0)); + assertEquals(aStyleRangeWithFontStyle(rangeB, SWT.ITALIC), resultingStyleRanges.get(1)); + assertEquals(aStyleRangeWithFontStyle(rangeC, SWT.BOLD | SWT.ITALIC), resultingStyleRanges.get(2)); + assertEquals(aStyleRangeWithFontStyle(rangeD, SWT.BOLD | SWT.ITALIC), resultingStyleRanges.get(3)); + } + + private List mergeStyleRanges(StyleRange[] existingRanges, StyleRange[] newRanges) { + TextPresentation textPresentation = new TextPresentation(); + textPresentation.replaceStyleRanges(existingRanges); + + StyleRangeHolder styleRangeHolder = new StyleRangeHolder(); + styleRangeHolder.saveStyles(List.of(newRanges)); + + StyleRangeMerger semanticMergeStrategy = new StyleRangeMerger(true, false); + semanticMergeStrategy.mergeStyleRanges(textPresentation, styleRangeHolder); + + List resultingStyleRanges = new ArrayList<>(); + textPresentation.getNonDefaultStyleRangeIterator().forEachRemaining(resultingStyleRanges::add); + return resultingStyleRanges; + } + + private StyleRange aStyleRangeWithFontStyle(IRegion region, int fontStyle) { + return new StyleRange(region.getOffset(), region.getLength(), null, null, fontStyle); + } + + private StyleRange aStyleRange(IRegion region, Color foreground, Color background, int fontStyle) { + return new StyleRange(region.getOffset(), region.getLength(), foreground, background, fontStyle); + } + + // works for both contiguous and non-contiguous a and b + private IRegion span(IRegion a, IRegion b) { + return new Region(a.getOffset(), b.getOffset() + b.getLength() - a.getOffset()); + } +} diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java index 775c39303..3affd8ca4 100644 --- a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/SemanticHighlightReconcilerStrategy.java @@ -105,9 +105,14 @@ public class SemanticHighlightReconcilerStrategy private @Nullable CompletableFuture> semanticTokensFullFuture; + private StyleRangeMerger merger; + public SemanticHighlightReconcilerStrategy() { IPreferenceStore store = LanguageServerPlugin.getDefault().getPreferenceStore(); disabled = store.getBoolean("semanticHighlightReconciler.disabled"); //$NON-NLS-1$ + boolean overrideBold = !store.getBoolean("semanticHighlightReconciler.ignoreBoldNormal"); //$NON-NLS-1$ + boolean overrideItalic = !store.getBoolean("semanticHighlightReconciler.ignoreItalicNormal"); //$NON-NLS-1$ + merger = new StyleRangeMerger(overrideBold, overrideItalic); } /** @@ -312,9 +317,6 @@ private void fullReconcileOnce() { @Override public void applyTextPresentation(final TextPresentation textPresentation) { documentTimestampAtLastAppliedTextPresentation = DocumentUtil.getDocumentModificationStamp(document); - IRegion extent = textPresentation.getExtent(); - if (extent != null && styleRangeHolder != null) { - textPresentation.mergeStyleRanges(styleRangeHolder.overlappingRanges(extent)); - } + merger.mergeStyleRanges(textPresentation, styleRangeHolder); } } diff --git a/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/StyleRangeMerger.java b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/StyleRangeMerger.java new file mode 100644 index 000000000..33bf36ead --- /dev/null +++ b/org.eclipse.lsp4e/src/org/eclipse/lsp4e/operations/semanticTokens/StyleRangeMerger.java @@ -0,0 +1,129 @@ +/******************************************************************************* + * Copyright (c) 2022 Avaloq Evolution AG. + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.lsp4e.operations.semanticTokens; + +import java.util.Iterator; + +import org.eclipse.jdt.annotation.NonNull; +import org.eclipse.jdt.annotation.Nullable; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.TextPresentation; +import org.eclipse.swt.SWT; +import org.eclipse.swt.custom.StyleRange; + +/** + * Merger to allow semantic highlighting to unset bold and/or italic + * (presumably set/owned by TM4E highlighting) when it is unset. + * + * The css {font-weight: normal} otherwise has no effect. As a side-effect, the + * css {} will also unset. + */ +public class StyleRangeMerger { + + private boolean unsetBoldWhenNotSet; + private boolean unsetItalicWhenNotSet; + + public StyleRangeMerger(boolean unsetBoldWhenNotSet, boolean unsetItalicWhenNotSet) { + this.unsetBoldWhenNotSet = unsetBoldWhenNotSet; + this.unsetItalicWhenNotSet = unsetItalicWhenNotSet; + } + + /** + * Merge style ranges from semantic highlighting into the existing text + * presentation. + *

+ * In addition to creating new style ranges and merging features from old and + * new (e.g. background color and foreground color), italic and bold will be + * unset if semantic highlighting does not set them. + * + * @param textPresentation + * the {@link TextPresentation} + * @param styleRangeHolder + * the semantic highlighting style ranges + */ + @SuppressWarnings("null") + public void mergeStyleRanges(final TextPresentation textPresentation, @Nullable StyleRangeHolder styleRangeHolder) { + final IRegion extent = textPresentation.getExtent(); + + if (extent == null || styleRangeHolder == null) { + return; + } + + StyleRange[] styleRanges = styleRangeHolder.overlappingRanges(extent); + + if (styleRanges.length == 0) { + return; + } + + // text presentation's merge will merge bold and italic with "or" -> this will + // not unset them + textPresentation.mergeStyleRanges(styleRanges); + + // style ranges are modified by TextPresentation merge, so fetch a new copy + styleRanges = styleRangeHolder.overlappingRanges(extent); + + // now that the style ranges have been merged into textPresentation each + // semantic styleRange has exact overlap with 1 or more in the textPresentation + // (exact means the first overlapping range has the same start and the last the + // same end) + Iterator e = textPresentation.getNonDefaultStyleRangeIterator(); + @NonNull + StyleRange target = e.next(); // as we merged a non-empty array of style ranges there is at least 1 + for (int idx = 0; idx < styleRanges.length; idx++) { + StyleRange template = styleRanges[idx]; + if (!isStyleModifying(template)) { + // only consider style ranges that potentially modify an existing style + continue; + } + + // find the target style range with the same start + while (target.start != template.start) { + target = e.next(); + } + + // apply modification until we have a style range at or after the end + int templateEnd = template.start + template.length; + do { + modifyStyle(target, template); + } while (e.hasNext() && (target = e.next()).start < templateEnd); + } + } + + /** + * Whether a given style must additionally modify beyond the result of textPresentation's merge. + * + * @param style + * @return + */ + protected boolean isStyleModifying(StyleRange style) { + int mask = SWT.NORMAL; + if (unsetBoldWhenNotSet) + mask |= SWT.BOLD; + if (unsetItalicWhenNotSet) + mask |= SWT.ITALIC; + return (style.fontStyle | mask) != style.fontStyle; + } + + + /** + * Apply necessary modifications from template to target. + * + * @param target the target style + * @param template the template style + */ + protected void modifyStyle(StyleRange target, StyleRange template) { + int mask = ~SWT.NORMAL; + if (unsetBoldWhenNotSet && (template.fontStyle | SWT.BOLD) != template.fontStyle) + mask &= ~SWT.BOLD; + if (unsetItalicWhenNotSet && (template.fontStyle | SWT.ITALIC) != template.fontStyle) + mask &= ~SWT.ITALIC; + target.fontStyle &= mask; + } + +} \ No newline at end of file