1 /*
  2  * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.  Oracle designates this
  8  * particular file as subject to the "Classpath" exception as provided
  9  * by Oracle in the LICENSE file that accompanied this code.
 10  *
 11  * This code is distributed in the hope that it will be useful, but WITHOUT
 12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 14  * version 2 for more details (a copy is included in the LICENSE file that
 15  * accompanied this code).
 16  *
 17  * You should have received a copy of the GNU General Public License version
 18  * 2 along with this work; if not, write to the Free Software Foundation,
 19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 20  *
 21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 22  * or visit www.oracle.com if you need additional information or have any
 23  * questions.
 24  */
 25 
 26 package jdk.incubator.code.parser.impl;
 27 
 28 import java.util.BitSet;
 29 
 30 /**
 31  * A class that defines source code positions as simple character
 32  * offsets from the beginning of the file. The first character
 33  * is at position 0.
 34  * <p>
 35  * Support is also provided for (line,column) coordinates, but tab
 36  * expansion is optional and no Unicode escape translation is considered.
 37  * The first character is at location (1,1).
 38  *
 39  * <p><b>This is NOT part of any supported API.
 40  * If you write code that depends on this, you do so at your own risk.
 41  * This code and its internal interfaces are subject to change or
 42  * deletion without notice.</b>
 43  */
 44 public final class Position {
 45     static final int NOPOS = -1;
 46 
 47     static final int FIRSTPOS = 0;
 48     static final int FIRSTLINE = 1;
 49     static final int FIRSTCOLUMN = 1;
 50 
 51     static final int LINESHIFT = 10;
 52     static final int MAXCOLUMN = (1 << LINESHIFT) - 1;
 53     static final int MAXLINE = (1 << (Integer.SIZE - LINESHIFT)) - 1;
 54 
 55     static final int MAXPOS = Integer.MAX_VALUE;
 56 
 57     /**
 58      * This is class is not supposed to be instantiated.
 59      */
 60     private Position() {
 61     }
 62 
 63     /**
 64      * A two-way map between line/column numbers and positions,
 65      * derived from a scan done at creation time.  Tab expansion is
 66      * optionally supported via a character map.  Text content
 67      * is not retained.
 68      * <p>
 69      * Notes:  The first character position FIRSTPOS is at
 70      * (FIRSTLINE,FIRSTCOLUMN).  No account is taken of Unicode escapes.
 71      *
 72      * @param src        Source characters
 73      * @param max        Number of characters to read
 74      * @param expandTabs If true, expand tabs when calculating columns
 75      */
 76     static LineMap makeLineMap(char[] src, int max, boolean expandTabs) {
 77         LineMapImpl lineMap = expandTabs ?
 78                 new LineTabMapImpl(max) : new LineMapImpl();
 79         lineMap.build(src, max);
 80         return lineMap;
 81     }
 82 
 83     /**
 84      * Encode line and column numbers in an integer as:
 85      * {@code line-number << LINESHIFT + column-number }.
 86      * {@link Position#NOPOS} represents an undefined position.
 87      *
 88      * @param line number of line (first is 1)
 89      * @param col  number of character on line (first is 1)
 90      * @return an encoded position or {@link Position#NOPOS}
 91      * if the line or column number is too big to
 92      * represent in the encoded format
 93      * @throws IllegalArgumentException if line or col is less than 1
 94      */
 95     static int encodePosition(int line, int col) {
 96         if (line < 1)
 97             throw new IllegalArgumentException("line must be greater than 0");
 98         if (col < 1)
 99             throw new IllegalArgumentException("column must be greater than 0");
100 
101         if (line > MAXLINE || col > MAXCOLUMN) {
102             return NOPOS;
103         }
104         return (line << LINESHIFT) + col;
105     }
106 
107     public interface LineMap {
108         /**
109          * Finds the start position of a line.
110          *
111          * @param line line number (beginning at 1)
112          * @return position of first character in line
113          * @throws IndexOutOfBoundsException if {@code lineNumber < 1}
114          *                                   if {@code lineNumber > no. of lines}
115          */
116         long getStartPosition(long line);
117 
118         /**
119          * Finds the position corresponding to a (line,column).
120          *
121          * @param line   line number (beginning at 1)
122          * @param column tab-expanded column number (beginning 1)
123          * @return position of character
124          * @throws IndexOutOfBoundsException if {@code line < 1}
125          *                                   if {@code line > no. of lines}
126          */
127         long getPosition(long line, long column);
128 
129         /**
130          * Finds the line containing a position; a line termination
131          * character is on the line it terminates.
132          *
133          * @param pos character offset of the position
134          * @return the line number of pos (first line is 1)
135          */
136         long getLineNumber(long pos);
137 
138         /**
139          * Finds the column for a character position.
140          * Tab characters preceding the position on the same line
141          * will be expanded when calculating the column number.
142          *
143          * @param pos character offset of the position
144          * @return the tab-expanded column number of pos (first column is 1)
145          */
146         long getColumnNumber(long pos);
147 
148         /**
149          * Find the start position of a line.
150          *
151          * @param line number of line (first is 1)
152          * @return position of first character in line
153          * @throws ArrayIndexOutOfBoundsException if {@code lineNumber < 1}
154          *                                        if {@code lineNumber > no. of lines}
155          */
156         int getStartPosition(int line);
157 
158         /**
159          * Find the position corresponding to a (line,column).
160          *
161          * @param line   number of line (first is 1)
162          * @param column number of character on line (first is 1)
163          * @return position of character
164          * @throws ArrayIndexOutOfBoundsException if {@code line < 1}
165          *                                        if {@code line > no. of lines}
166          */
167         int getPosition(int line, int column);
168 
169         /**
170          * Find the line containing a position; a line termination
171          * character is on the line it terminates.
172          *
173          * @param pos character offset of the position
174          * @return the line number on which pos occurs (first line is 1)
175          */
176         int getLineNumber(int pos);
177 
178         /**
179          * Find the column for a character position.
180          * Note:  this method does not handle tab expansion.
181          * If tab expansion is needed, use a LineTabMap instead.
182          *
183          * @param pos character offset of the position
184          * @return the column number at which pos occurs
185          */
186         int getColumnNumber(int pos);
187     }
188 
189     static class LineMapImpl implements LineMap {
190         int[] startPosition; // start position of each line
191 
192         LineMapImpl() {
193         }
194 
195         void build(char[] src, int max) {
196             int c = 0;
197             int i = 0;
198             int[] linebuf = new int[max];
199             while (i < max) {
200                 linebuf[c++] = i;
201                 do {
202                     char ch = src[i];
203                     if (ch == '\r' || ch == '\n') {
204                         if (ch == '\r' && (i + 1) < max && src[i + 1] == '\n')
205                             i += 2;
206                         else
207                             ++i;
208                         break;
209                     } else if (ch == '\t')
210                         setTabPosition(i);
211                 } while (++i < max);
212             }
213             this.startPosition = new int[c];
214             System.arraycopy(linebuf, 0, startPosition, 0, c);
215         }
216 
217         @Override
218         public int getStartPosition(int line) {
219             return startPosition[line - FIRSTLINE];
220         }
221 
222         @Override
223         public long getStartPosition(long line) {
224             return getStartPosition(longToInt(line));
225         }
226 
227         @Override
228         public int getPosition(int line, int column) {
229             return startPosition[line - FIRSTLINE] + column - FIRSTCOLUMN;
230         }
231 
232         @Override
233         public long getPosition(long line, long column) {
234             return getPosition(longToInt(line), longToInt(column));
235         }
236 
237         // Cache of last line number lookup
238         private int lastPosition = Position.FIRSTPOS;
239         private int lastLine = Position.FIRSTLINE;
240 
241         @Override
242         public int getLineNumber(int pos) {
243             if (pos == lastPosition) {
244                 return lastLine;
245             }
246             lastPosition = pos;
247 
248             int low = 0;
249             int high = startPosition.length - 1;
250             while (low <= high) {
251                 int mid = (low + high) >> 1;
252                 int midVal = startPosition[mid];
253 
254                 if (midVal < pos)
255                     low = mid + 1;
256                 else if (midVal > pos)
257                     high = mid - 1;
258                 else {
259                     lastLine = mid + 1; // pos is at beginning of this line
260                     return lastLine;
261                 }
262             }
263             lastLine = low;
264             return lastLine;  // pos is on this line
265         }
266 
267         @Override
268         public long getLineNumber(long pos) {
269             return getLineNumber(longToInt(pos));
270         }
271 
272         @Override
273         public int getColumnNumber(int pos) {
274             return pos - startPosition[getLineNumber(pos) - FIRSTLINE] + FIRSTCOLUMN;
275         }
276 
277         @Override
278         public long getColumnNumber(long pos) {
279             return getColumnNumber(longToInt(pos));
280         }
281 
282         static int longToInt(long longValue) {
283             int intValue = (int) longValue;
284             if (intValue != longValue)
285                 throw new IndexOutOfBoundsException();
286             return intValue;
287         }
288 
289         void setTabPosition(int offset) {
290         }
291     }
292 
293     /**
294      * A LineMap that handles tab expansion correctly.  The cost is
295      * an additional bit per character in the source array.
296      */
297     static class LineTabMapImpl extends LineMapImpl {
298         private final BitSet tabMap;       // bits set for tab positions.
299 
300         LineTabMapImpl(int max) {
301             super();
302             tabMap = new BitSet(max);
303         }
304 
305         @Override
306         void setTabPosition(int offset) {
307             tabMap.set(offset);
308         }
309 
310         @Override
311         public int getColumnNumber(int pos) {
312             int lineStart = startPosition[getLineNumber(pos) - FIRSTLINE];
313             int column = 0;
314             for (int bp = lineStart; bp < pos; bp++) {
315                 if (tabMap.get(bp))
316                     column = tabulate(column);
317                 else
318                     column++;
319             }
320             return column + FIRSTCOLUMN;
321         }
322 
323         @Override
324         public int getPosition(int line, int column) {
325             int pos = startPosition[line - FIRSTLINE];
326             column -= FIRSTCOLUMN;
327             int col = 0;
328             while (col < column) {
329                 pos++;
330                 if (tabMap.get(pos))
331                     col = tabulate(col);
332                 else
333                     col++;
334             }
335             return pos;
336         }
337     }
338 
339     /**
340      * Tabulator column increment.
341      */
342     static final int TabInc = 8;
343 
344     /**
345      * Bump column to the next tab.
346      */
347     static int tabulate(int column) {
348         return (column / TabInc * TabInc) + TabInc;
349     }
350 }