changeset 16130:6f7d3f3703d3

Truffle/Source: - LineLocation and LineBreakpoint no longer implement Comparable - TextMap now internal to the Source factory
author Michael Van De Vanter <michael.van.de.vanter@oracle.com>
date Mon, 16 Jun 2014 20:52:06 -0700
parents a3a9d703c078
children a4a2147a6aa6
files graal/com.oracle.truffle.api.test/src/com/oracle/truffle/api/test/utilities/SourceTextTest.java graal/com.oracle.truffle.api.test/src/com/oracle/truffle/api/test/utilities/TextMapTest.java graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/LineLocation.java graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/Source.java graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/TextMap.java
diffstat 5 files changed, 368 insertions(+), 388 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.api.test/src/com/oracle/truffle/api/test/utilities/SourceTextTest.java	Mon Jun 16 20:52:06 2014 -0700
@@ -0,0 +1,202 @@
+/*
+ * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.api.test.utilities;
+
+import static org.junit.Assert.*;
+
+import org.junit.*;
+
+import com.oracle.truffle.api.source.*;
+
+public class SourceTextTest {
+
+    private final Source emptySource = Source.fromText("", null);
+
+    private final Source emptyLineSource = Source.fromText("\n", null);
+
+    private final Source shortSource = Source.fromText("01", null);
+
+    private final Source longSource = Source.fromText("01234\n67\n9\n", null);
+
+    @Test
+    public void emptyTextTest0() {
+        assertEquals(emptySource.getLineCount(), 0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyTextTest1() {
+        emptySource.getLineNumber(0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyTextTest2() {
+        emptySource.getColumnNumber(0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyTextTest3() {
+        emptySource.getLineNumber(-1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyTextTest4() {
+        emptySource.getLineStartOffset(0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyTextTest5() {
+        emptySource.getLineStartOffset(1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyTextTest6() {
+        emptySource.getLineLength(1);
+    }
+
+    @Test
+    public void emptyLineTest0() {
+        assertEquals(emptyLineSource.getLineCount(), 1);
+        assertEquals(emptyLineSource.getLineNumber(0), 1);
+        assertEquals(emptyLineSource.getLineStartOffset(1), 0);
+        assertEquals(emptyLineSource.getColumnNumber(0), 1);
+        assertEquals(emptyLineSource.getLineLength(1), 0);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyLineTest1() {
+        emptyLineSource.getLineNumber(1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyLineTest2() {
+        emptyLineSource.getLineStartOffset(2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyLineTest3() {
+        emptyLineSource.getColumnNumber(1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void emptyLineTest4() {
+        emptyLineSource.getLineLength(2);
+    }
+
+    @Test
+    public void shortTextTest0() {
+
+        assertEquals(shortSource.getLineCount(), 1);
+
+        assertEquals(shortSource.getLineNumber(0), 1);
+        assertEquals(shortSource.getLineStartOffset(1), 0);
+        assertEquals(shortSource.getColumnNumber(0), 1);
+
+        assertEquals(shortSource.getLineNumber(1), 1);
+        assertEquals(shortSource.getColumnNumber(1), 2);
+
+        assertEquals(shortSource.getLineLength(1), 2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shortTextTest1() {
+        shortSource.getLineNumber(-1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shortTextTest2() {
+        shortSource.getColumnNumber(-1);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shortTextTest3() {
+        shortSource.getLineNumber(2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shortTextTest4() {
+        shortSource.getColumnNumber(2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shortTextTest5() {
+        shortSource.getLineLength(2);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void shortTextTest6() {
+        shortSource.getLineLength(2);
+    }
+
+    @Test
+    public void longTextTest0() {
+
+        assertEquals(longSource.getLineCount(), 3);
+
+        assertEquals(longSource.getLineNumber(0), 1);
+        assertEquals(longSource.getLineStartOffset(1), 0);
+        assertEquals(longSource.getColumnNumber(0), 1);
+
+        assertEquals(longSource.getLineNumber(4), 1);
+        assertEquals(longSource.getColumnNumber(4), 5);
+
+        assertEquals(longSource.getLineNumber(5), 1); // newline
+        assertEquals(longSource.getColumnNumber(5), 6); // newline
+        assertEquals(longSource.getLineLength(1), 5);
+
+        assertEquals(longSource.getLineNumber(6), 2);
+        assertEquals(longSource.getLineStartOffset(2), 6);
+        assertEquals(longSource.getColumnNumber(6), 1);
+
+        assertEquals(longSource.getLineNumber(7), 2);
+        assertEquals(longSource.getColumnNumber(7), 2);
+
+        assertEquals(longSource.getLineNumber(8), 2); // newline
+        assertEquals(longSource.getLineNumber(8), 2); // newline
+        assertEquals(longSource.getLineLength(2), 2);
+
+        assertEquals(longSource.getLineNumber(9), 3);
+        assertEquals(longSource.getLineStartOffset(3), 9);
+        assertEquals(longSource.getColumnNumber(9), 1);
+
+        assertEquals(longSource.getLineNumber(10), 3); // newline
+        assertEquals(longSource.getColumnNumber(10), 2); // newline
+        assertEquals(longSource.getLineLength(3), 1);
+
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void longTextTest1() {
+        longSource.getLineNumber(11);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void longTextTest2() {
+        longSource.getColumnNumber(11);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void longTextTest3() {
+        longSource.getLineStartOffset(4);
+    }
+
+}
--- a/graal/com.oracle.truffle.api.test/src/com/oracle/truffle/api/test/utilities/TextMapTest.java	Mon Jun 16 23:07:45 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,202 +0,0 @@
-/*
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package com.oracle.truffle.api.test.utilities;
-
-import static org.junit.Assert.*;
-
-import org.junit.*;
-
-import com.oracle.truffle.api.source.*;
-
-public class TextMapTest {
-
-    final TextMap emptyTextMap = new TextMap("");
-
-    final TextMap emptyLineMap = new TextMap("\n");
-
-    private final TextMap shortMap = new TextMap("01");
-
-    private final TextMap longMap = new TextMap("01234\n67\n9\n");
-
-    @Test
-    public void emptyTextTest0() {
-        assertEquals(emptyTextMap.lineCount(), 0);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyTextTest1() {
-        emptyTextMap.offsetToLine(0);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyTextTest2() {
-        emptyTextMap.offsetToCol(0);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyTextTest3() {
-        emptyTextMap.lineStartOffset(-1);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyTextTest4() {
-        emptyTextMap.lineStartOffset(0);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyTextTest5() {
-        emptyTextMap.lineStartOffset(1);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyTextTest6() {
-        emptyTextMap.lineLength(1);
-    }
-
-    @Test
-    public void emptyLineTest0() {
-        assertEquals(emptyLineMap.lineCount(), 1);
-        assertEquals(emptyLineMap.offsetToLine(0), 1);
-        assertEquals(emptyLineMap.lineStartOffset(1), 0);
-        assertEquals(emptyLineMap.offsetToCol(0), 1);
-        assertEquals(emptyLineMap.lineLength(1), 0);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyLineTest1() {
-        emptyLineMap.offsetToLine(1);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyLineTest2() {
-        emptyLineMap.lineStartOffset(2);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyLineTest3() {
-        emptyLineMap.offsetToCol(1);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void emptyLineTest4() {
-        emptyLineMap.lineLength(2);
-    }
-
-    @Test
-    public void shortTextTest0() {
-
-        assertEquals(shortMap.lineCount(), 1);
-
-        assertEquals(shortMap.offsetToLine(0), 1);
-        assertEquals(shortMap.lineStartOffset(1), 0);
-        assertEquals(shortMap.offsetToCol(0), 1);
-
-        assertEquals(shortMap.offsetToLine(1), 1);
-        assertEquals(shortMap.offsetToCol(1), 2);
-
-        assertEquals(shortMap.lineLength(1), 2);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void shortTextTest1() {
-        shortMap.offsetToLine(-1);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void shortTextTest2() {
-        shortMap.offsetToCol(-1);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void shortTextTest3() {
-        shortMap.offsetToLine(2);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void shortTextTest4() {
-        shortMap.offsetToCol(2);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void shortTextTest5() {
-        shortMap.lineStartOffset(2);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void shortTextTest6() {
-        shortMap.lineLength(2);
-    }
-
-    @Test
-    public void longTextTest0() {
-
-        assertEquals(longMap.lineCount(), 3);
-
-        assertEquals(longMap.offsetToLine(0), 1);
-        assertEquals(longMap.lineStartOffset(1), 0);
-        assertEquals(longMap.offsetToCol(0), 1);
-
-        assertEquals(longMap.offsetToLine(4), 1);
-        assertEquals(longMap.offsetToCol(4), 5);
-
-        assertEquals(longMap.offsetToLine(5), 1); // newline
-        assertEquals(longMap.offsetToCol(5), 6); // newline
-        assertEquals(longMap.lineLength(1), 5);
-
-        assertEquals(longMap.offsetToLine(6), 2);
-        assertEquals(longMap.lineStartOffset(2), 6);
-        assertEquals(longMap.offsetToCol(6), 1);
-
-        assertEquals(longMap.offsetToLine(7), 2);
-        assertEquals(longMap.offsetToCol(7), 2);
-
-        assertEquals(longMap.offsetToLine(8), 2); // newline
-        assertEquals(longMap.offsetToLine(8), 2); // newline
-        assertEquals(longMap.lineLength(2), 2);
-
-        assertEquals(longMap.offsetToLine(9), 3);
-        assertEquals(longMap.lineStartOffset(3), 9);
-        assertEquals(longMap.offsetToCol(9), 1);
-
-        assertEquals(longMap.offsetToLine(10), 3); // newline
-        assertEquals(longMap.offsetToCol(10), 2); // newline
-        assertEquals(longMap.lineLength(3), 1);
-
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void longTextTest1() {
-        longMap.offsetToLine(11);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void longTextTest2() {
-        longMap.offsetToCol(11);
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void longTextTest3() {
-        longMap.lineStartOffset(4);
-    }
-
-}
--- a/graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/LineLocation.java	Mon Jun 16 23:07:45 2014 +0200
+++ b/graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/LineLocation.java	Mon Jun 16 20:52:06 2014 -0700
@@ -29,7 +29,7 @@
  * instance of {@link Source}, suitable for hash table keys with equality defined in terms of
  * content.
  */
-public interface LineLocation extends Comparable<Object> {
+public interface LineLocation {
 
     public Source getSource();
 
--- a/graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/Source.java	Mon Jun 16 23:07:45 2014 +0200
+++ b/graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/Source.java	Mon Jun 16 20:52:06 2014 -0700
@@ -194,10 +194,10 @@
         return builder.toString();
     }
 
-    protected Source() {
+    Source() {
     }
 
-    protected TextMap textMap = null;
+    private TextMap textMap = null;
 
     protected abstract void reset();
 
@@ -266,23 +266,38 @@
     /**
      * Given a 0-based character offset, return the 1-based number of the line that includes the
      * position.
+     *
+     * @throws IllegalArgumentException if the offset is outside the text contents
      */
-    public final int getLineNumber(int offset) {
+    public final int getLineNumber(int offset) throws IllegalArgumentException {
         return checkTextMap().offsetToLine(offset);
     }
 
     /**
-     * Given a 1-based line number, return the 0-based offset of the first character in the line.
+     * Given a 0-based character offset, return the 1-based number of the column at the position.
+     *
+     * @throws IllegalArgumentException if the offset is outside the text contents
      */
-    public final int getLineStartOffset(int lineNumber) {
+    public final int getColumnNumber(int offset) throws IllegalArgumentException {
+        return checkTextMap().offsetToCol(offset);
+    }
+
+    /**
+     * Given a 1-based line number, return the 0-based offset of the first character in the line.
+     *
+     * @throws IllegalArgumentException if there is no such line in the text
+     */
+    public final int getLineStartOffset(int lineNumber) throws IllegalArgumentException {
         return checkTextMap().lineStartOffset(lineNumber);
     }
 
     /**
      * The number of characters (not counting a possible terminating newline) in a (1-based)
      * numbered line.
+     *
+     * @throws IllegalArgumentException if there is no such line in the text
      */
-    public final int getLineLength(int lineNumber) {
+    public final int getLineLength(int lineNumber) throws IllegalArgumentException {
         return checkTextMap().lineLength(lineNumber);
     }
 
@@ -809,15 +824,152 @@
             return source.equals(other.source);
         }
 
-        @Override
-        public int compareTo(Object o) {
-            final LineLocationImpl other = (LineLocationImpl) o;
-            final int nameOrder = source.getName().compareTo(other.source.getName());
-            if (nameOrder != 0) {
-                return nameOrder;
+    }
+
+    /**
+     * A utility for converting between coordinate systems in a string of text interspersed with
+     * newline characters. The coordinate systems are:
+     * <ul>
+     * <li>0-based character offset from the beginning of the text, where newline characters count
+     * as a single character and the first character in the text occupies position 0.</li>
+     * <li>1-based position in the 2D space of lines and columns, in which the first position in the
+     * text is at (1,1).</li>
+     * </ul>
+     * <p>
+     * This utility is based on positions occupied by characters, not text stream positions as in a
+     * text editor. The distinction shows up in editors where you can put the cursor just past the
+     * last character in a buffer; this is necessary, among other reasons, so that you can put the
+     * edit cursor in a new (empty) buffer. For the purposes of this utility, however, there are no
+     * character positions in an empty text string and there are no lines in an empty text string.
+     * <p>
+     * A newline character designates the end of a line and occupies a column position.
+     * <p>
+     * If the text ends with a character other than a newline, then the characters following the
+     * final newline character count as a line, even though not newline-terminated.
+     * <p>
+     * <strong>Limitations:</strong>
+     * <ul>
+     * <li>Does not handle multiple character encodings correctly.</li>
+     * <li>Treats tabs as occupying 1 column.</li>
+     * <li>Does not handle multiple-character line termination sequences correctly.</li>
+     * </ul>
+     */
+    private static final class TextMap {
+
+        // 0-based offsets of newline characters in the text, with sentinel
+        private final int[] nlOffsets;
+
+        // The number of characters in the text, including newlines (which count as 1).
+        private final int textLength;
+
+        // Is the final text character a newline?
+        final boolean finalNL;
+
+        /**
+         * Constructs map permitting translation between 0-based character offsets and 1-based
+         * lines/columns.
+         */
+        public TextMap(String text) {
+            this.textLength = text.length();
+            final ArrayList<Integer> lines = new ArrayList<>();
+            lines.add(0);
+            int offset = 0;
+
+            while (offset < text.length()) {
+                final int nlIndex = text.indexOf('\n', offset);
+                if (nlIndex >= 0) {
+                    offset = nlIndex + 1;
+                    lines.add(offset);
+                } else {
+                    break;
+                }
+            }
+            lines.add(Integer.MAX_VALUE);
+
+            nlOffsets = new int[lines.size()];
+            for (int line = 0; line < lines.size(); line++) {
+                nlOffsets[line] = lines.get(line);
             }
-            return Integer.compare(line, other.line);
+
+            finalNL = textLength > 0 && (textLength == nlOffsets[nlOffsets.length - 2]);
+        }
+
+        /**
+         * Converts 0-based character offset to 1-based number of the line containing the character.
+         *
+         * @throws IllegalArgumentException if the offset is outside the string.
+         */
+        public int offsetToLine(int offset) throws IllegalArgumentException {
+            if (offset < 0 || offset >= textLength) {
+                throw new IllegalArgumentException("offset out of bounds");
+            }
+            int line = 1;
+            while (offset >= nlOffsets[line]) {
+                line++;
+            }
+            return line;
+        }
+
+        /**
+         * Converts 0-based character offset to 1-based number of the column occupied by the
+         * character.
+         * <p>
+         * Tabs are not expanded; they occupy 1 column.
+         *
+         * @throws IllegalArgumentException if the offset is outside the string.
+         */
+        public int offsetToCol(int offset) throws IllegalArgumentException {
+            return 1 + offset - nlOffsets[offsetToLine(offset) - 1];
+        }
+
+        /**
+         * The number of lines in the text; if characters appear after the final newline, then they
+         * also count as a line, even though not newline-terminated.
+         */
+        public int lineCount() {
+            if (textLength == 0) {
+                return 0;
+            }
+            return finalNL ? nlOffsets.length - 2 : nlOffsets.length - 1;
+        }
+
+        /**
+         * Converts 1-based line number to the 0-based offset of the line's first character; this
+         * would be the offset of a newline if the line is empty.
+         *
+         * @throws IllegalArgumentException if there is no such line in the text.
+         */
+        public int lineStartOffset(int line) throws IllegalArgumentException {
+            if (textLength == 0 || lineOutOfRange(line)) {
+                throw new IllegalArgumentException("line out of bounds");
+            }
+            return nlOffsets[line - 1];
+        }
+
+        /**
+         * Gets the number of characters in a line, identified by 1-based line number;
+         * <em>does not</em> include the final newline, if any.
+         *
+         * @throws IllegalArgumentException if there is no such line in the text.
+         */
+        public int lineLength(int line) throws IllegalArgumentException {
+            if (textLength == 0 || lineOutOfRange(line)) {
+                throw new IllegalArgumentException("line out of bounds");
+            }
+            if (line == nlOffsets.length - 1 && !finalNL) {
+                return textLength - nlOffsets[line - 1];
+            }
+            return (nlOffsets[line] - nlOffsets[line - 1]) - 1;
+
+        }
+
+        /**
+         * Is the line number out of range.
+         */
+        private boolean lineOutOfRange(int line) {
+            return line <= 0 || line >= nlOffsets.length || (line == nlOffsets.length - 1 && finalNL);
         }
 
     }
+
 }
--- a/graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/TextMap.java	Mon Jun 16 23:07:45 2014 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,172 +0,0 @@
-/*
- * Copyright (c) 2013, Oracle and/or its affiliates. All rights reserved.
- * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
- *
- * This code is free software; you can redistribute it and/or modify it
- * under the terms of the GNU General Public License version 2 only, as
- * published by the Free Software Foundation.  Oracle designates this
- * particular file as subject to the "Classpath" exception as provided
- * by Oracle in the LICENSE file that accompanied this code.
- *
- * This code is distributed in the hope that it will be useful, but WITHOUT
- * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
- * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
- * version 2 for more details (a copy is included in the LICENSE file that
- * accompanied this code).
- *
- * You should have received a copy of the GNU General Public License version
- * 2 along with this work; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
- *
- * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
- * or visit www.oracle.com if you need additional information or have any
- * questions.
- */
-package com.oracle.truffle.api.source;
-
-import java.util.*;
-
-/**
- * A utility for converting between coordinate systems in a string of text interspersed with newline
- * characters. The coordinate systems are:
- * <ul>
- * <li>0-based character offset from the beginning of the text, where newline characters count as a
- * single character and the first character in the text occupies position 0.</li>
- * <li>1-based position in the 2D space of lines and columns, in which the first position in the
- * text is at (1,1).</li>
- * </ul>
- * <p>
- * This utility is based on positions occupied by characters, not text stream positions as in a text
- * editor. The distinction shows up in editors where you can put the cursor just past the last
- * character in a buffer; this is necessary, among other reasons, so that you can put the edit
- * cursor in a new (empty) buffer. For the purposes of this utility, however, there are no character
- * positions in an empty text string and there are no lines in an empty text string.
- * <p>
- * A newline character designates the end of a line and occupies a column position.
- * <p>
- * If the text ends with a character other than a newline, then the characters following the final
- * newline character count as a line, even though not newline-terminated.
- * <p>
- * <strong>Limitations:</strong>
- * <ul>
- * <li>Does not handle multiple character encodings correctly.</li>
- * <li>Treats tabs as occupying 1 column.</li>
- * <li>Does not handle multiple-character line termination sequences correctly.</li>
- * </ul>
- */
-public final class TextMap {
-
-    // 0-based offsets of newline characters in the text, with sentinel
-    private final int[] nlOffsets;
-
-    // The number of characters in the text, including newlines (which count as 1).
-    private final int textLength;
-
-    // Is the final text character a newline?
-    final boolean finalNL;
-
-    /**
-     * Constructs map permitting translation between 0-based character offsets and 1-based
-     * lines/columns.
-     */
-    public TextMap(String text) {
-        this.textLength = text.length();
-        final ArrayList<Integer> lines = new ArrayList<>();
-        lines.add(0);
-        int offset = 0;
-
-        while (offset < text.length()) {
-            final int nlIndex = text.indexOf('\n', offset);
-            if (nlIndex >= 0) {
-                offset = nlIndex + 1;
-                lines.add(offset);
-            } else {
-                break;
-            }
-        }
-        lines.add(Integer.MAX_VALUE);
-
-        nlOffsets = new int[lines.size()];
-        for (int line = 0; line < lines.size(); line++) {
-            nlOffsets[line] = lines.get(line);
-        }
-
-        finalNL = textLength > 0 && (textLength == nlOffsets[nlOffsets.length - 2]);
-    }
-
-    /**
-     * Converts 0-based character offset to 1-based number of the line containing the character.
-     * 
-     * @throws IllegalArgumentException if the offset is outside the string.
-     */
-    public int offsetToLine(int offset) throws IllegalArgumentException {
-        if (offset < 0 || offset >= textLength) {
-            throw new IllegalArgumentException("offset out of bounds");
-        }
-        int line = 1;
-        while (offset >= nlOffsets[line]) {
-            line++;
-        }
-        return line;
-    }
-
-    /**
-     * Converts 0-based character offset to 1-based number of the column occupied by the character.
-     * <p>
-     * Tabs are not expanded; they occupy 1 column.
-     * 
-     * @throws IllegalArgumentException if the offset is outside the string.
-     */
-    public int offsetToCol(int offset) throws IllegalArgumentException {
-        return 1 + offset - nlOffsets[offsetToLine(offset) - 1];
-    }
-
-    /**
-     * The number of lines in the text; if characters appear after the final newline, then they also
-     * count as a line, even though not newline-terminated.
-     */
-    public int lineCount() {
-        if (textLength == 0) {
-            return 0;
-        }
-        return finalNL ? nlOffsets.length - 2 : nlOffsets.length - 1;
-    }
-
-    /**
-     * Converts 1-based line number to the 0-based offset of the line's first character; this would
-     * be the offset of a newline if the line is empty.
-     * 
-     * @throws IllegalArgumentException if there is no such line in the text.
-     */
-    public int lineStartOffset(int line) throws IllegalArgumentException {
-        if (textLength == 0 || lineOutOfRange(line)) {
-            throw new IllegalArgumentException("line out of bounds");
-        }
-        return nlOffsets[line - 1];
-    }
-
-    /**
-     * Gets the number of characters in a line, identified by 1-based line number; <em>does not</em>
-     * include the final newline, if any.
-     * 
-     * @throws IllegalArgumentException if there is no such line in the text.
-     */
-    public int lineLength(int line) throws IllegalArgumentException {
-        if (textLength == 0 || lineOutOfRange(line)) {
-            throw new IllegalArgumentException("line out of bounds");
-        }
-        if (line == nlOffsets.length - 1 && !finalNL) {
-            return textLength - nlOffsets[line - 1];
-        }
-        return (nlOffsets[line] - nlOffsets[line - 1]) - 1;
-
-    }
-
-    /**
-     * Is the line number out of range.
-     */
-    private boolean lineOutOfRange(int line) {
-        return line <= 0 || line >= nlOffsets.length || (line == nlOffsets.length - 1 && finalNL);
-    }
-
-}