Skip to content

Commit

Permalink
Allow semantic highlighting to unset bold and italic (#1153)
Browse files Browse the repository at this point in the history
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"
  • Loading branch information
gregdyke authored Dec 5, 2024
1 parent 14ca7fb commit 803ae83
Show file tree
Hide file tree
Showing 3 changed files with 319 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -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<StyleRange> 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<StyleRange> 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<StyleRange> 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<StyleRange> 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<StyleRange> 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<StyleRange> 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());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,14 @@ public class SemanticHighlightReconcilerStrategy

private @Nullable CompletableFuture<Optional<VersionedSemanticTokens>> 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);
}

/**
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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<StyleRange> 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;
}

}

0 comments on commit 803ae83

Please sign in to comment.