// Copyright 2012 Google Inc. All Rights Reserved. // // 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. package com.google.collide.client.document.linedimensions; import com.google.collide.client.document.linedimensions.LineDimensionsCalculator.RoundingStrategy; import com.google.collide.client.editor.search.SearchTestsUtil; import com.google.collide.shared.document.Document; import com.google.collide.shared.document.Line; import com.google.collide.shared.document.LineInfo; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import junit.framework.TestCase; /** * Tests for {@link LineDimensionsCalculator}. */ public class LineDimensionsCalculatorTests extends TestCase { /** * An object which provides measurement information to a * {@link LineDimensionsCalculator}. */ public static class TestMeasurementProvider implements MeasurementProvider { private final int characterWidth; public TestMeasurementProvider(int characterWidth) { this.characterWidth = characterWidth; } @Override public double getCharacterWidth() { return characterWidth; } @Override public double measureStringWidth(String text) { int length = 0; for (int i = 0; i < text.length(); i++) { char c = text.charAt(i); int utype = Character.getType(c); if (utype == Character.COMBINING_SPACING_MARK || utype == Character.NON_SPACING_MARK || utype == Character.ENCLOSING_MARK) { // These characters are 0, width (we do this so its clear) length += 0; } else if (c == '\t') { length += TAB_SIZE; } else if (c == '\r') { length += 0; } else if (c < 255) { length += 1; } else { /* * if its not a combining mark, and its not standard Latin, make it * just like a tab. This is just for testing purposes, and isn't * usually the case. */ length += TAB_SIZE; } } return length * getCharacterWidth(); } } /** Tab size in columns. */ private static final int TAB_SIZE = 2; /** Width of a character in pixels. */ private static final int CHARACTER_SIZE = 8; private LineDimensionsCalculator calculator; private MeasurementProvider measurementProvider; private Document basicDocument; private Document indentAndCarriageReturnDocument; private Document fullUnicodeDocument; @Override public void setUp() { measurementProvider = new TestMeasurementProvider(CHARACTER_SIZE); calculator = LineDimensionsCalculator.createWithCustomProvider(measurementProvider); LineDimensionsUtils.setTabSpaceEquivalence(TAB_SIZE); basicDocument = Document.createFromString(Joiner.on('\n').join(BASIC_NO_SPECIAL_DOCUMENT)); indentAndCarriageReturnDocument = Document.createFromString(Joiner.on('\n').join(TAB_AND_CARRIAGE_RETURN_DOCUMENT)); fullUnicodeDocument = Document.createFromString(Joiner.on('\n').join(FULL_UNICODE_DOCUMENT)); } public void testSettingDocumentDoesNothingToDocument() { calculator.handleDocumentChange(basicDocument); LineInfo lineInfo = basicDocument.getFirstLineInfo(); do { assertTag(null, lineInfo.line()); } while (lineInfo.moveToNext()); } public void testLinesAreLazilyTagged() { calculator.handleDocumentChange(basicDocument); LineInfo lineTwo = SearchTestsUtil.gotoLineInfo(basicDocument, 2); LineInfo lineThree = SearchTestsUtil.gotoLineInfo(basicDocument, 3); calculator.convertColumnToX(lineTwo.line(), 3); calculator.convertColumnToX(lineThree.line(), 3); LineInfo lineInfo = basicDocument.getFirstLineInfo(); do { if (lineInfo.number() == lineTwo.number() || lineInfo.number() == lineThree.number()) { assertTag(false, lineInfo.line()); } else { assertTag(null, lineInfo.line()); } } while (lineInfo.moveToNext()); } public void testCalculatedCorrectlyForSimpleCase() { calculator.handleDocumentChange(basicDocument); LineInfo lineInfo = SearchTestsUtil.gotoLineInfo(basicDocument, 2); double x = assertReversibleAndReturnX(lineInfo.line(), 3); assertEquals(naiveColumnToX(3), x); x = assertReversibleAndReturnX(lineInfo.line(), 0); assertEquals(0.0, x); x = assertReversibleAndReturnX(lineInfo.line(), lineInfo.line().length() - 1); assertEquals(naiveColumnToX(lineInfo.line().length() - 1), x); } public void testIndentationAndCarriageReturnDoesNotAddOffsetCache() { calculator.handleDocumentChange(indentAndCarriageReturnDocument); LineInfo lineInfo = indentAndCarriageReturnDocument.getFirstLineInfo(); do { calculator.convertColumnToX(lineInfo.line(), 3); assertTag(false, lineInfo.line()); } while (lineInfo.moveToNext()); } public void testIndentationHandled() { calculator.handleDocumentChange(indentAndCarriageReturnDocument); LineInfo lineInfo = indentAndCarriageReturnDocument.getFirstLineInfo(); double x = assertReversibleAndReturnX(lineInfo.line(), 0); assertEquals(0.0, x); x = assertReversibleAndReturnX(lineInfo.line(), 1); assertWideChars(1, 1, x); x = assertReversibleAndReturnX(lineInfo.line(), 2); assertWideChars(2, 2, x); x = assertReversibleAndReturnX(lineInfo.line(), 3); assertWideChars(3, 3, x); x = assertReversibleAndReturnX(lineInfo.line(), 4); assertWideChars(4, 3, x); x = assertReversibleAndReturnX(lineInfo.line(), 4); assertWideChars(4, 3, x); int lastColumn = lineInfo.line().length() - 1; x = assertReversibleAndReturnX(lineInfo.line(), lastColumn); assertWideChars(lastColumn, 3, x); } public void testCarriageReturnHandled() { calculator.handleDocumentChange(indentAndCarriageReturnDocument); LineInfo lineInfo = SearchTestsUtil.gotoLineInfo(indentAndCarriageReturnDocument, 1); double x = assertReversibleAndReturnX(lineInfo.line(), 0); assertEquals(0.0, x); x = assertReversibleAndReturnX(lineInfo.line(), 5); assertEquals(naiveColumnToX(5), x); x = assertReversibleAndReturnX(lineInfo.line(), 15); assertEquals(naiveColumnToX(15), x); // Test offset due to carriage return is correct int length = lineInfo.line().length(); x = assertReversibleAndReturnX(lineInfo.line(), length - 3); assertWideCharsAndZeroWidthChars(length - 3, 0, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineInfo.line(), length - 2, 1); assertWideCharsAndZeroWidthChars(length - 2, 0, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineInfo.line(), length - 1, 0); assertWideCharsAndZeroWidthChars(length - 1, 0, 1, x); } public void testBothTabAndCarriageReturn() { calculator.handleDocumentChange(indentAndCarriageReturnDocument); LineInfo lineTwo = SearchTestsUtil.gotoLineInfo(indentAndCarriageReturnDocument, 2); double x = assertReversibleAndReturnX(lineTwo.line(), 1); assertWideChars(1, 1, x); x = assertReversibleAndReturnX(lineTwo.line(), 2); assertWideChars(2, 1, x); x = assertReversibleAndReturnX(lineTwo.line(), 10); assertWideChars(10, 1, x); // Test offset due to carriage return is correct int length = lineTwo.line().length(); x = assertReversibleAndReturnX(lineTwo.line(), length - 3); assertWideCharsAndZeroWidthChars(length - 3, 1, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineTwo.line(), length - 2, 1); assertWideCharsAndZeroWidthChars(length - 2, 1, 0, x); x = assertReversibleAndReturnX(lineTwo.line(), length - 1); assertWideCharsAndZeroWidthChars(length - 1, 1, 1, x); } public void testLineWithAllTabsAndCarriageReturn() { calculator.handleDocumentChange(indentAndCarriageReturnDocument); LineInfo lineThree = SearchTestsUtil.gotoLineInfo(indentAndCarriageReturnDocument, 3); double x = assertReversibleAndReturnX(lineThree.line(), 1); assertWideChars(1, 1, x); x = assertReversibleAndReturnX(lineThree.line(), 2); assertWideChars(2, 2, x); x = assertReversibleAndReturnX(lineThree.line(), 3); assertWideChars(3, 3, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineThree.line(), 4, 1); assertWideChars(4, 4, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineThree.line(), 5, 0); assertWideCharsAndZeroWidthChars(5, 4, 1, x); } public void testLineWithAllTabsAndCarriageReturnWithTabSizeOfThree() { LineDimensionsUtils.setTabSpaceEquivalence(3); testLineWithAllTabsAndCarriageReturn(); testBothTabAndCarriageReturn(); } public void testAssertAllLinesWithSpecialCharsHaveATag() { LineInfo lineInfo = fullUnicodeDocument.getFirstLineInfo(); do { calculator.convertColumnToX(lineInfo.line(), 3); assertTag(true, lineInfo.line()); } while (lineInfo.moveToNext()); } public void testAssertLineOneOfUnicodeDocIsRight() { calculator.handleDocumentChange(fullUnicodeDocument); LineInfo lineOne = fullUnicodeDocument.getFirstLineInfo(); double x = assertReversibleAndReturnX(lineOne.line(), 1); assertWideChars(1, 1, x); x = assertReversibleAndReturnX(lineOne.line(), 7); assertWideChars(7, 1, x); x = assertReversibleAndReturnX(lineOne.line(), 8); assertWideChars(8, 2, x); // Test Carriage Return int length = lineOne.line().length(); x = assertReversibleAndReturnX(lineOne.line(), length - 4); assertWideCharsAndZeroWidthChars(length - 4, 2, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineOne.line(), length - 3, 2); assertWideCharsAndZeroWidthChars(length - 3, 2, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineOne.line(), length - 2, 1); assertWideCharsAndZeroWidthChars(length - 2, 2, 1, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineOne.line(), length - 1, 0); assertWideCharsAndZeroWidthChars(length - 1, 2, 2, x); } public void testAssertLineTwoOfUnicodeDocIsRight() { calculator.handleDocumentChange(fullUnicodeDocument); LineInfo lineTwo = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 1); for (int i = 0; i < lineTwo.line().length(); i++) { double x = assertReversibleAndReturnX(lineTwo.line(), i); assertWideChars(i, i, x); } } public void testAssertLineThreeOfUNicodeDocIsRight() { calculator.handleDocumentChange(fullUnicodeDocument); LineInfo lineThree = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 2); double x = assertReversibleAndReturnX(lineThree.line(), 0); assertEquals(0.0, x); for (int i = 1, j = 2; i < lineThree.line().length() - 2; i += 2, j += 2) { x = assertReversibleAndReturnX(lineThree.line(), i); assertWideChars(i, i - 1, x); x = assertReversibleAndReturnX(lineThree.line(), j); assertWideChars(j, j - 1, x); } // not dealing with \n btw int lastCharIndex = lineThree.line().length() - 1; x = assertReversibleAndReturnX(lineThree.line(), lastCharIndex); /* * so this looks funny so I'll comment it but its just convenient. it's * saying that given lastCharIndex column, it has 2 less widechars then its * column index. This makes sense because if every column before it was a * wide char then we'd have myIndex - 1 wide chars. */ assertWideChars(lastCharIndex, lastCharIndex - 2, x); } public void testAssertLineFourOfUnicodeDocIsRight() { calculator.handleDocumentChange(fullUnicodeDocument); LineInfo lineFour = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 3); double x = assertReversibleAndReturnX(lineFour.line(), 0); assertEquals(0.0, x); // The first character is an a + a ` combining mark x = assertReversibleAndReturnXAccountingForZeroWidth(lineFour.line(), 1, 1); assertWideCharsAndZeroWidthChars(1, 0, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineFour.line(), 2, 0); assertWideCharsAndZeroWidthChars(2, 0, 1, x); // Test remaining characters x = assertReversibleAndReturnX(lineFour.line(), 3); assertWideCharsAndZeroWidthChars(3, 0, 1, x); x = assertReversibleAndReturnX(lineFour.line(), 4); assertWideCharsAndZeroWidthChars(4, 0, 1, x); } public void testAssertLineFiveOfUnicodeDocIsRight() { calculator.handleDocumentChange(fullUnicodeDocument); /* * This line looks like LLccLLccLL * NOTE: These characters all appear double wide since the test measurer * just blatently makes any character > 255 double wide. In realty arabic * characters aren't like that and present other challenges related to size. */ LineInfo lineFive = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 4); double x = assertReversibleAndReturnX(lineFive.line(), 0); assertEquals(0.0, x); x = assertReversibleAndReturnX(lineFive.line(), 1); assertWideCharsAndZeroWidthChars(1, 1, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineFive.line(), 2, 2); assertWideCharsAndZeroWidthChars(2, 2, 0, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineFive.line(), 3, 1); assertWideCharsAndZeroWidthChars(3, 2, 1, x); x = assertReversibleAndReturnX(lineFive.line(), 4); assertWideCharsAndZeroWidthChars(4, 2, 2, x); x = assertReversibleAndReturnX(lineFive.line(), 5); assertWideCharsAndZeroWidthChars(5, 3, 2, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineFive.line(), 6, 2); assertWideCharsAndZeroWidthChars(6, 4, 2, x); x = assertReversibleAndReturnXAccountingForZeroWidth(lineFive.line(), 7, 1); assertWideCharsAndZeroWidthChars(7, 4, 3, x); x = assertReversibleAndReturnX(lineFive.line(), 8); assertWideCharsAndZeroWidthChars(8, 4, 4, x); x = assertReversibleAndReturnX(lineFive.line(), 9); assertWideCharsAndZeroWidthChars(9, 5, 4, x); x = assertReversibleAndReturnX(lineFive.line(), 10); assertWideCharsAndZeroWidthChars(10, 6, 4, x); } public void testAssertLineSixWithMultipleCombiningMarksWorks() { calculator.handleDocumentChange(fullUnicodeDocument); // This string is a````=à LineInfo lineSix = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 5); double x = assertReversibleAndReturnX(lineSix.line(), 0); assertEquals(0.0, x); // a` x = assertReversibleAndReturnXAccountingForZeroWidth(lineSix.line(), 1, 4); assertWideCharsAndZeroWidthChars(1, 0, 0, x); // a`` x = assertReversibleAndReturnXAccountingForZeroWidth(lineSix.line(), 2, 3); assertWideCharsAndZeroWidthChars(2, 0, 1, x); // a``` x = assertReversibleAndReturnXAccountingForZeroWidth(lineSix.line(), 3, 2); assertWideCharsAndZeroWidthChars(3, 0, 2, x); // a```, if you do this I hate you x = assertReversibleAndReturnXAccountingForZeroWidth(lineSix.line(), 4, 1); assertWideCharsAndZeroWidthChars(4, 0, 3, x); // a````= x = assertReversibleAndReturnX(lineSix.line(), 5); assertWideCharsAndZeroWidthChars(5, 0, 4, x); // a````=à x = assertReversibleAndReturnX(lineSix.line(), 6); assertWideCharsAndZeroWidthChars(6, 0, 4, x); } public void testTextMutationsMarkCacheDirtyWithoutCombiningMarks() { calculator.handleDocumentChange(fullUnicodeDocument); // The second line is all katakana characters so no combining marks LineInfo lineTwo = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 1); // we want to build the cache so 'll just ask for a column at the end's x calculator.convertColumnToX(lineTwo.line(), lineTwo.line().length() - 1); // Lets perform a delete and ensure all is still working :) fullUnicodeDocument.deleteText(lineTwo.line(), 5, 1); double x = assertReversibleAndReturnX(lineTwo.line(), 4); assertWideChars(4, 4, x); x = assertReversibleAndReturnX(lineTwo.line(), 5); assertWideChars(5, 5, x); x = assertReversibleAndReturnX(lineTwo.line(), 6); assertWideChars(6, 6, x); // Lets perform a non-special insertion. fullUnicodeDocument.insertText(lineTwo.line(), 5, "alex"); x = assertReversibleAndReturnX(lineTwo.line(), 4); assertWideChars(4, 4, x); x = assertReversibleAndReturnX(lineTwo.line(), 5); assertWideChars(5, 5, x); x = assertReversibleAndReturnX(lineTwo.line(), 6); assertWideChars(6, 5, x); x = assertReversibleAndReturnX(lineTwo.line(), 7); assertWideChars(7, 5, x); } public void testMutationsMakesNewLine() { calculator.handleDocumentChange(fullUnicodeDocument); // The second line is all katakana characters so no combining marks LineInfo lineInfo = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 1); // we want to build the cache so 'll just ask for a column at the end's x calculator.convertColumnToX(lineInfo.line(), lineInfo.line().length() - 1); // Lets perform a non-special insertion. fullUnicodeDocument.insertText(lineInfo.line(), 5, "al\nex"); double x = assertReversibleAndReturnX(lineInfo.line(), 4); assertWideChars(4, 4, x); x = assertReversibleAndReturnX(lineInfo.line(), 5); assertWideChars(5, 5, x); x = assertReversibleAndReturnX(lineInfo.line(), 6); assertWideChars(6, 5, x); x = assertReversibleAndReturnX(lineInfo.line(), 7); assertWideChars(7, 5, x); // Check the new line that was created works right lineInfo.moveToNext(); x = assertReversibleAndReturnX(lineInfo.line(), 1); assertWideChars(1, 0, x); x = assertReversibleAndReturnX(lineInfo.line(), 2); assertWideChars(2, 0, x); x = assertReversibleAndReturnX(lineInfo.line(), 3); assertWideChars(3, 1, x); x = assertReversibleAndReturnX(lineInfo.line(), 4); assertWideChars(4, 2, x); } public void testCorrectWhenMutationsAroundZeroWidthCharacters() { calculator.handleDocumentChange(fullUnicodeDocument); // We use the accented a from line three for these tests LineInfo lineFour = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 3); // we want to build the cache so I'll just ask for a column at the end's x calculator.convertColumnToX(lineFour.line(), lineFour.line().length() - 1); // delete the grave accent combining mark. cache should remove the entry // from a as well. fullUnicodeDocument.deleteText(lineFour.line(), 1, 1); double x = assertReversibleAndReturnX(lineFour.line(), 1); assertWideChars(1, 0, x); // We do some inserting of zero-width grave accents so we can test the // multi-combining mark case (the closest I can get to Arabic craziness). fullUnicodeDocument.insertText(lineFour.line(), 1, "\u0300\u0300\u0300"); // rebuild cache again calculator.convertColumnToX(lineFour.line(), lineFour.line().length() - 1); // delete the last mark fullUnicodeDocument.deleteText(lineFour.line(), 3, 1); // Assert all is well, and we measure correctly x = assertReversibleAndReturnXAccountingForZeroWidth(lineFour.line(), 2, 1); assertWideCharsAndZeroWidthChars(2, 0, 1, x); } public void testConvertingXToColumn() { calculator.handleDocumentChange(fullUnicodeDocument); // All characters in this line are double-wide. LineInfo lineTwo = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 1); // we loop through skipping the \n for (int i = 0; i < lineTwo.line().length() - 1; i++) { assertXToColumn(lineTwo.line(), i, CHARACTER_SIZE * 2 * i, CHARACTER_SIZE * 2 * (i + 1)); } LineInfo lineThree = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 3); assertXToColumn(lineThree.line(), 0, 0, CHARACTER_SIZE); // a // =, we bypass the ` automagically since it can't be clicked on assertXToColumn(lineThree.line(), 2, CHARACTER_SIZE, CHARACTER_SIZE * 2); // à assertXToColumn(lineThree.line(), 3, CHARACTER_SIZE * 2, CHARACTER_SIZE * 3); LineInfo lineFive = SearchTestsUtil.gotoLineInfo(fullUnicodeDocument, 5); assertXToColumn(lineFive.line(), 0, 0, CHARACTER_SIZE); // a // =, skip ```` assertXToColumn(lineFive.line(), 5, CHARACTER_SIZE, CHARACTER_SIZE * 2); } /** * Asserts that a range of x's maps to its corresponding column correctly. */ private void assertXToColumn(Line line, int column, double leftEdgeX, double rightEdgeX) { double characterWidth = rightEdgeX - leftEdgeX; for (double x = leftEdgeX; x < rightEdgeX; x++) { for (RoundingStrategy roundingStrategy : RoundingStrategy.values()) { int expectedColumn = roundingStrategy.apply(column + (x - leftEdgeX) / characterWidth); int resultColumn = calculator.convertXToColumn(line, x, roundingStrategy); assertEquals(expectedColumn, resultColumn); } } } /** * Converts a column to it's x position. Columns are 0-based. */ private double naiveColumnToX(int column) { return measurementProvider.getCharacterWidth() * column; } /** * Converts a column to its x coordinate then converts the x coordinate back * to a column ensuring it matches. * * @return the x coordinate of the column so it can be tested further. */ private double assertReversibleAndReturnX(Line line, int column) { return assertReversibleAndReturnXAccountingForZeroWidth(line, column, 0); } /** * Converts a column to its x coordinate then converts the x coordinate back * to a column ensuring it matches. This method accounts for any zero width * characters so that the assertion when converting x back to column will be * correct. * * @param contiguousZeroWidthChars number of contiguous zero width characters * to the right of the current column (inclusive). * * @return the x coordinate of the column so it can be tested further. */ private double assertReversibleAndReturnXAccountingForZeroWidth( Line line, int column, int contiguousZeroWidthChars) { double x = calculator.convertColumnToX(line, column); assertEquals(column + contiguousZeroWidthChars, calculator.convertXToColumn(line, x, RoundingStrategy.FLOOR)); return x; } /** * @see #assertWideCharsAndZeroWidthChars(int, int, int, double) */ private void assertWideChars(int column, int wideChars, double x) { assertWideCharsAndZeroWidthChars(column, wideChars, 0, x); } /** * @param wideChars Number of tabs * @param zeroWidthCharsToLeft Number of zero-width characters to the left of * the column (exclusive). */ private void assertWideCharsAndZeroWidthChars( int column, int wideChars, int zeroWidthCharsToLeft, double x) { assertEquals(naiveColumnToX(LineDimensionsUtils.getTabWidth() * wideChars) + naiveColumnToX(column - wideChars - zeroWidthCharsToLeft), x); } private static void assertTag(Boolean expected, Line line) { Boolean tag = line.getTag(LineDimensionsUtils.NEEDS_OFFSET_LINE_TAG); assertEquals("Line " + line.getText(), expected, tag); } public static final ImmutableList<String> BASIC_NO_SPECIAL_DOCUMENT = ImmutableList.of("Listen my children, and you shall hear", "Of the midnight ride of Paul Reveare", "On the eighteenth of April in seventy-five", "Hardly a man was now alive", "Who remembers that fateful day and year."); public static final ImmutableList<String> TAB_AND_CARRIAGE_RETURN_DOCUMENT = ImmutableList.of("\t\t\tSome people see a problem and think,", "'I know I'll use regular expressions!'.\r", "\tNow they have two problems\r", // Whoever types in this line is a real sob.... "\t\t\t\t\r", "This line intentionally left blank so previous line gets a \\n w/o me typing it :)"); public static final ImmutableList<String> FULL_UNICODE_DOCUMENT = ImmutableList.of("\tsimple\tline\r\r", // middle tab forces offset cache. "烏烏龍茶烏茶龍龍茶烏龍茶", "8\t♜\t♞\t♝\t♛\t♚\t♝\t♞\t♜8", // first a is a ` combining mark, second is a single char "à=à", // ahhh!!!!! Goes LLccLLccLL "لضَّالِّين", // this is an a + ` x 4 "à̀̀̀=à"); }