Skip to content

Commit b0df8b4

Browse files
tobias-melchertobiasmelcher
authored andcommitted
Handle zero-width characters properly in code minings
This change improves the rendering of code minings in the presence of zero-width Unicode characters (e.g., ZWSP, ZWNJ, ZWJ, and BOM). The `InlinedAnnotationDrawingStrategy` was updated to recognize these characters, adjust their width calculations, and properly place code mining annotations without causing layout issues. This change is needed by #1437
1 parent 5c0e81b commit b0df8b4

3 files changed

Lines changed: 76 additions & 6 deletions

File tree

bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/inlined/InlinedAnnotationDrawingStrategy.java

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
*/
1414
package org.eclipse.jface.text.source.inlined;
1515

16+
import java.util.Set;
17+
1618
import org.eclipse.swt.SWT;
1719
import org.eclipse.swt.custom.StyleRange;
1820
import org.eclipse.swt.custom.StyledText;
@@ -43,6 +45,16 @@ class InlinedAnnotationDrawingStrategy implements IDrawingStrategy {
4345

4446
private final ITextViewer viewer;
4547

48+
private static final char ZW_SPACE= '\u200b';
49+
50+
private static final char ZW_NON_JOINER= '\u200c';
51+
52+
private static final char ZW_JOINER= '\u200d';
53+
54+
private static final char ZW_NO_BREAK_SPACE= '\ufeff';
55+
56+
private static final Set<Character> ZW_CHARACTERS= Set.of(ZW_SPACE, ZW_NON_JOINER, ZW_JOINER, ZW_NO_BREAK_SPACE);
57+
4658
public InlinedAnnotationDrawingStrategy(ITextViewer viewer) {
4759
this.viewer = viewer;
4860
}
@@ -436,13 +448,17 @@ protected static void drawAsLeftOf1stCharacter(LineContentAnnotation annotation,
436448
// Get size of the character where GlyphMetrics width is added
437449
Point charBounds= gc.stringExtent(hostCharacter);
438450
int charWidth= charBounds.x;
439-
if (charWidth == 0 && ("\r".equals(hostCharacter) || "\n".equals(hostCharacter))) { //$NON-NLS-1$ //$NON-NLS-2$
451+
boolean isZeroWidthCharacter= false;
452+
if (hostCharacter.length() == 1 && ZW_CHARACTERS.contains(hostCharacter.charAt(0))) {
453+
isZeroWidthCharacter= true;
454+
charWidth= 0;
455+
} else if (charWidth == 0 && ("\r".equals(hostCharacter) || "\n".equals(hostCharacter))) { //$NON-NLS-1$ //$NON-NLS-2$
440456
// charWidth is 0 for '\r' on font Consolas, but not on other fonts, why?
441457
charWidth= gc.stringExtent(" ").x; //$NON-NLS-1$
442458
}
443459
// FIXME: remove this code when we need not redraw the character (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=531769)
444460
// START TO REMOVE
445-
annotation.setRedrawnCharacterWidth(charWidth);
461+
annotation.setRedrawnCharacterWidth(charWidth, isZeroWidthCharacter);
446462
// END TO REMOVE
447463

448464
// Annotation takes place, add GlyphMetrics width to the style
@@ -523,6 +539,11 @@ protected static void drawAsRightOfPreviousCharacter(LineContentAnnotation annot
523539
char hostCharacter= textWidget.getText(widgetOffset - 1, widgetOffset - 1).charAt(0);
524540
// use gc.stringExtent instead of gc.geCharWidth because of bug 548866
525541
int redrawnCharacterWidth= hostCharacter != '\t' ? gc.stringExtent(Character.toString(hostCharacter)).x : textWidget.getTabs() * gc.stringExtent(" ").x; //$NON-NLS-1$
542+
boolean isZeroWidthCharacter= false;
543+
if (ZW_CHARACTERS.contains(hostCharacter)) {
544+
redrawnCharacterWidth= 0;
545+
isZeroWidthCharacter= true;
546+
}
526547
Rectangle charBounds= textWidget.getTextBounds(widgetOffset - 1, widgetOffset - 1);
527548
Rectangle annotationBounds= new Rectangle(charBounds.x + redrawnCharacterWidth, charBounds.y, annotation.getWidth(), charBounds.height);
528549

@@ -539,7 +560,7 @@ protected static void drawAsRightOfPreviousCharacter(LineContentAnnotation annot
539560
if (width != 0) {
540561
// FIXME: remove this code when we need not redraw the character (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=531769)
541562
// START TO REMOVE
542-
annotation.setRedrawnCharacterWidth(redrawnCharacterWidth);
563+
annotation.setRedrawnCharacterWidth(redrawnCharacterWidth, isZeroWidthCharacter);
543564
// END TO REMOVE
544565

545566
// Annotation takes place, add GlyphMetrics width to the style

bundles/org.eclipse.jface.text/src/org/eclipse/jface/text/source/inlined/LineContentAnnotation.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ public class LineContentAnnotation extends AbstractInlinedAnnotation {
4444

4545
private int redrawnCharacterWidth;
4646

47+
private boolean isZeroWidthCharacter;
48+
4749
/**
4850
* Line content annotation constructor.
4951
*
@@ -113,8 +115,9 @@ int getRedrawnCharacterWidth() {
113115
return redrawnCharacterWidth;
114116
}
115117

116-
void setRedrawnCharacterWidth(int redrawnCharacterWidth) {
118+
void setRedrawnCharacterWidth(int redrawnCharacterWidth, boolean isZeroWidthCharacter) {
117119
this.redrawnCharacterWidth= redrawnCharacterWidth;
120+
this.isZeroWidthCharacter= isZeroWidthCharacter;
118121
}
119122

120123
@Override
@@ -151,7 +154,7 @@ StyleRange updateStyle(StyleRange style, FontMetrics fontMetrics, ITextViewer vi
151154
if (!afterPosition) {
152155
usePreviousChar= drawRightToPreviousChar(widgetPosition.getOffset(), textWidget);
153156
}
154-
if (width == 0 || getRedrawnCharacterWidth() == 0) {
157+
if (isZeroWidthCharacter == false && (width == 0 || getRedrawnCharacterWidth() == 0)) {
155158
return null;
156159
}
157160
int fullWidth= width + getRedrawnCharacterWidth();

tests/org.eclipse.jface.text.tests/src/org/eclipse/jface/text/tests/codemining/CodeMiningTest.java

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.junit.jupiter.api.Assumptions;
2828
import org.junit.jupiter.api.BeforeEach;
2929
import org.junit.jupiter.api.Test;
30-
3130
import org.junit.jupiter.api.extension.ExtensionContext;
3231
import org.junit.jupiter.api.extension.RegisterExtension;
3332
import org.junit.jupiter.api.extension.TestWatcher;
@@ -512,6 +511,53 @@ protected boolean condition() {
512511
}.waitForCondition(widget.getDisplay(), 1000), "Code mining is unexpectedly rendered below last line");
513512
}
514513

514+
@Test
515+
public void testCodeMiningOnZeroWitdhCharacterAfterPosition() {
516+
codeMiningOnZeroWidthCharacter(true);
517+
}
518+
519+
@Test
520+
public void testCodeMiningOnZeroWidthCharacterBeforePosition() {
521+
codeMiningOnZeroWidthCharacter(false);
522+
}
523+
524+
private void codeMiningOnZeroWidthCharacter(boolean afterPosition) {
525+
char ZW_SPACE= '\u200b';
526+
String text= "a" + ZW_SPACE + "b";
527+
fViewer.getDocument().set(text);
528+
fViewer.setCodeMiningProviders(new ICodeMiningProvider[] {
529+
new AbstractCodeMiningProvider() {
530+
@Override
531+
public CompletableFuture<List<? extends ICodeMining>> provideCodeMinings(ITextViewer viewer, IProgressMonitor monitor) {
532+
List<ICodeMining> result= new ArrayList<>();
533+
result.add(new LineContentCodeMining(new Position(1, 1), afterPosition, this) {
534+
@Override
535+
public String getLabel() {
536+
return "ZWSP";
537+
}
538+
});
539+
return CompletableFuture.completedFuture(result);
540+
}
541+
}
542+
});
543+
fViewer.updateCodeMinings();
544+
Assertions.assertTrue(new DisplayHelper() {
545+
@Override
546+
protected boolean condition() {
547+
var tw= fViewer.getTextWidget();
548+
int off= afterPosition ? 1 : 0;
549+
StyleRange styleRange= tw.getStyleRangeAtOffset(off);
550+
if (styleRange != null && styleRange.metrics != null) {
551+
// code mining applied a style range with metrics at offset 1
552+
return true;
553+
} else {
554+
tw.redraw();
555+
}
556+
return false;
557+
}
558+
}.waitForCondition(fViewer.getTextWidget().getDisplay(), 1000), "Line content code mining not rendered at zero width character");
559+
}
560+
515561
@Test
516562
public void testCodeMiningSwitchingBetweenInLineAndLineHeader() {
517563
String ref= "REF-X";

0 commit comments

Comments
 (0)