diff graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/Source.java @ 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 74e142bd2b12
children 7109baa7b9eb
line wrap: on
line diff
--- 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);
         }
 
     }
+
 }