diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java index d750fcd4bc60..47f245207a47 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/view/ComplexTextFragment.java @@ -44,40 +44,41 @@ class ComplexTextFragment extends TextFragment { float totalWidth = (float)myGlyphVector.getGlyphPosition(numGlyphs).getX(); myCharPositions[numChars - 1] = totalWidth; int lastCharIndex = -1; - int ligatureStartCharIndex = 0; float lastX = isRtl ? totalWidth : 0; float prevX = lastX; - // Here we determine coordinates for boundaries between characters. + // Here we determine coordinates for boundaries between characters. // They will be used to place caret, last boundary coordinate is also defining the width of text fragment. // // We expect these positions to be ordered, so that when caret moves through text characters in some direction, corresponding text // offsets change monotonously (within the same-directionality fragment). // // Special case that we must account for is a ligature, when several adjacent characters are represented as a single glyph. - // In a glyph vector this glyph is associated with the first character, other characters are associated with empty glyphs. + // In a glyph vector this glyph is associated with the first character, + // other characters either don't have an associated glyph, or they are associated with empty glyphs. // (in RTL case real glyph will be associated with first logical character, i.e. last visual character) for (int i = 0; i < numGlyphs; i++) { int visualGlyphIndex = isRtl ? numGlyphs - 1 - i : i; int charIndex = myGlyphVector.getGlyphCharIndex(visualGlyphIndex); if (charIndex > lastCharIndex) { Rectangle2D bounds = myGlyphVector.getGlyphLogicalBounds(visualGlyphIndex).getBounds2D(); - if (bounds.isEmpty()) { - for (int j = ligatureStartCharIndex; j <= charIndex; j++) { - setCharPosition(j, prevX + (lastX - prevX) * (j - ligatureStartCharIndex + 1) / (charIndex - ligatureStartCharIndex + 1), - isRtl, numChars); + if (!bounds.isEmpty()) { + if (charIndex > lastCharIndex + 1) { + for (int j = Math.max(0, lastCharIndex); j < charIndex; j++) { + setCharPosition(j, prevX + (lastX - prevX) * (j - lastCharIndex + 1) / (charIndex - lastCharIndex), isRtl, numChars); + } } - } - else { float newX = isRtl ? Math.min(lastX, (float)bounds.getMinX()) : Math.max(lastX, (float)bounds.getMaxX()); newX = Math.max(0, Math.min(totalWidth, newX)); - ligatureStartCharIndex = lastCharIndex + 1; - for (int j = ligatureStartCharIndex; j <= charIndex; j++) { - setCharPosition(j, newX, isRtl, numChars); - } + setCharPosition(charIndex, newX, isRtl, numChars); prevX = lastX; lastX = newX; + lastCharIndex = charIndex; } - lastCharIndex = charIndex; + } + } + if (lastCharIndex < numChars - 1) { + for (int j = Math.max(0, lastCharIndex); j < numChars - 1; j++) { + setCharPosition(j, prevX + (lastX - prevX) * (j - lastCharIndex + 1) / (numChars - lastCharIndex), isRtl, numChars); } } } diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java index 5f24c40a87b4..61150248f5d4 100644 --- a/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java +++ b/platform/platform-tests/testSrc/com/intellij/openapi/editor/impl/view/ComplexTextFragmentTest.java @@ -25,9 +25,11 @@ import java.awt.*; import java.awt.font.FontRenderContext; import java.awt.font.GlyphVector; import java.awt.geom.Point2D; +import java.util.Objects; +import java.util.stream.Stream; -import static org.junit.Assert.assertArrayEquals; import static com.intellij.openapi.editor.impl.AbstractEditorTest.*; +import static org.junit.Assert.assertArrayEquals; public class ComplexTextFragmentTest { @Test @@ -49,7 +51,7 @@ public class ComplexTextFragmentTest { @Test public void testLigature() { assertCaretPositionsForGlyphVector( - glyph(0, 12).glyph(12, 12).glyph(12, 12), + glyph(0, 12).glyph(12, 12).glyph(12, 12), // ICU-style ligature (when all characters get a glyph) 4, 8, 12 ); } @@ -57,12 +59,36 @@ public class ComplexTextFragmentTest { @Test public void testRtlLigature() { assertCaretPositionsForGlyphVector( - rtl().glyph(0, 0).glyph(0, 0).glyph(0, 12), + rtl().glyph(0, 0).glyph(0, 0).glyph(0, 12), // ICU-style ligature (when all characters get a glyph) 4, 8, 12 ); } - - private static void assertCaretPositionsForGlyphVector(GlyphVector gv, int... expectedPositions) { + + @Test + public void testHbLigature() { + assertCaretPositionsForGlyphVector( + glyph(0, 12).noGlyph().noGlyph(), // Harfbuzz-style ligature (some characters don't map to glyphs) + 4, 8, 12 + ); + } + + @Test + public void testHbRtlLigature() { + assertCaretPositionsForGlyphVector( + rtl().noGlyph().noGlyph().glyph(0, 12), // Harfbuzz-style ligature (some characters don't map to glyphs) + 4, 8, 12 + ); + } + + @Test + public void testHbLigatureTwoChars() { + assertCaretPositionsForGlyphVector( + glyph(0, 8).noGlyph(), // Harfbuzz-style ligature (some characters don't map to glyphs) + 4, 8 + ); + } + + private static void assertCaretPositionsForGlyphVector(MyGlyphVector gv, int... expectedPositions) { FontLayoutService.setInstance(new MockFontLayoutService(TEST_CHAR_WIDTH, TEST_LINE_HEIGHT, TEST_DESCENT) { @NotNull @Override @@ -72,8 +98,7 @@ public class ComplexTextFragmentTest { } }); try { - // assuming one glyph per character - int length = gv.getNumGlyphs(); + int length = gv.getNumChars(); char[] text = new char[length]; ComplexTextFragment fragment = new ComplexTextFragment(text, 0, length, (gv.getLayoutFlags() & GlyphVector.FLAG_RUN_RTL) != 0, new Font(null), new FontRenderContext(null, false, false)); @@ -89,19 +114,19 @@ public class ComplexTextFragmentTest { } private static MyGlyphVector rtl() { - return new MyGlyphVector(true, new int[0], new int[0]); + return new MyGlyphVector(true, new Integer[0], new Integer[0]); } private static MyGlyphVector glyph(int xStart, int xEnd) { - return new MyGlyphVector(false, new int[]{xStart}, new int[]{xEnd - xStart}); + return new MyGlyphVector(false, new Integer[]{xStart}, new Integer[]{xEnd - xStart}); } - + private static class MyGlyphVector extends AbstractMockGlyphVector { private final boolean myRtl; - private final int[] myGlyphPositions; - private final int[] myGlyphWidths; + private final Integer[] myGlyphPositions; + private final Integer[] myGlyphWidths; - private MyGlyphVector(boolean rtl, int[] glyphPositions, int[] glyphWidths) { + private MyGlyphVector(boolean rtl, Integer[] glyphPositions, Integer[] glyphWidths) { assertTrue(glyphPositions.length == glyphWidths.length); myRtl = rtl; myGlyphPositions = glyphPositions; @@ -112,26 +137,44 @@ public class ComplexTextFragmentTest { return new MyGlyphVector(myRtl, ArrayUtil.append(myGlyphPositions, xStart), ArrayUtil.append(myGlyphWidths, xEnd - xStart)); } + private MyGlyphVector noGlyph() { + return new MyGlyphVector(myRtl, ArrayUtil.append(myGlyphPositions, null), ArrayUtil.append(myGlyphWidths, null)); + } + @Override public int getNumGlyphs() { + return (int)Stream.of(myGlyphPositions).filter(Objects::nonNull).count(); + } + + private int getNumChars() { return myGlyphPositions.length; } @Override public Point2D getGlyphPosition(int glyphIndex) { - return new Point(glyphIndex == myGlyphPositions.length - ? myGlyphPositions[glyphIndex - 1] + myGlyphWidths[glyphIndex - 1] - : myGlyphPositions[glyphIndex], 0); + boolean afterLast = glyphIndex == getNumGlyphs(); + int index = getGlyphIndexInArray(glyphIndex - (afterLast ? 1 : 0)); + return new Point(myGlyphPositions[index] + (afterLast ? myGlyphWidths[index] : 0), 0); } @Override public Shape getGlyphLogicalBounds(int glyphIndex) { - return new Rectangle(myGlyphPositions[glyphIndex], -TEST_DESCENT, myGlyphWidths[glyphIndex], TEST_LINE_HEIGHT); + int index = getGlyphIndexInArray(glyphIndex); + return new Rectangle(myGlyphPositions[index], -TEST_DESCENT, myGlyphWidths[index], TEST_LINE_HEIGHT); } @Override public int getGlyphCharIndex(int glyphIndex) { - return myRtl ? myGlyphPositions.length - 1 - glyphIndex : glyphIndex; + int index = getGlyphIndexInArray(glyphIndex); + return myRtl ? getNumChars() - 1 - index : index; + } + + private int getGlyphIndexInArray(int glyphIndex) { + int index = 0; + for (int i = 0; i < myGlyphPositions.length; i++) { + if (myGlyphPositions[i] != null && index++ == glyphIndex) return i; + } + return -1; } @Override