/* NSC -- new Scala compiler * Copyright 2005-2009 LAMP/EPFL * @author Martin Odersky */ // $Id: SourceFile.scala 18406 2009-07-30 07:41:22Z odersky $ package scala.tools.nsc package util import scala.tools.nsc.io.{AbstractFile, VirtualFile} import scala.collection.mutable.ArrayBuffer import annotation.tailrec object SourceFile { // Be very careful touching these. // Apparently trivial changes to the way you write these constants // will cause Scanners.scala to go from a nice efficient switch to // a ghastly nested if statement which will bring the type checker // to its knees. See ticket #1456 final val LF = '\u000A' final val FF = '\u000C' final val CR = '\u000D' final val SU = '\u001A' def isLineBreakChar(c: Int) = c match { case LF|FF|CR|SU => true case _ => false } } /** abstract base class of a source file used in the compiler */ abstract class SourceFile { import SourceFile._ def content : Array[Char] // normalized, must end in SU def file : AbstractFile def isLineBreak(idx : Int) : Boolean def length : Int def position(offset: Int) : Position = { assert(offset < length) new OffsetPosition(this, offset) } def position(line: Int, column: Int) : Position = new OffsetPosition(this, lineToOffset(line) + column) def offsetToLine(offset: Int): Int def lineToOffset(index : Int): Int /** Map a position to a position in the underlying source file. * For regular source files, simply return the argument. */ def positionInUltimateSource(position: Position) = position override def toString(): String = file.name /* + ":" + content.length */ def dbg(offset: Int) = (new OffsetPosition(this, offset)).dbgString def path = file.path def beginsWith(offset: Int, text: String): Boolean = (content drop offset) startsWith text def lineToString(index: Int): String = content drop lineToOffset(index) takeWhile (c => !isLineBreakChar(c)) mkString @tailrec final def skipWhitespace(offset: Int): Int = if (content(offset).isWhitespace) skipWhitespace(offset + 1) else offset def identifier(pos: Position, compiler: Global): Option[String] = None } /** a file whose contents do not change over time */ class BatchSourceFile(val file : AbstractFile, val content: Array[Char]) extends SourceFile { import SourceFile._ def this(_file: AbstractFile) = this(_file, _file.toCharArray) def this(sourceName: String, cs: Seq[Char]) = this(new VirtualFile(sourceName), cs.toArray) def this(file: AbstractFile, cs: Seq[Char]) = this(file, cs.toArray) override def equals(that : Any) = that match { case that : BatchSourceFile => file == that.file case _ => false } override def hashCode = file.hashCode val length = content.length // in SourceFileFragments, these are overridden to compensate during offset calculation // Invariant: length + start = underlyingLength def underlyingLength = length def start = 0 override def identifier(pos: Position, compiler: Global) = if (pos.isDefined && pos.source == this && pos.point != -1) { def isOK(c: Char) = { import compiler.syntaxAnalyzer.{ isOperatorPart, isIdentifierPart } isIdentifierPart(c) || isOperatorPart(c) } Some(new String(content drop pos.point takeWhile isOK)) } else { super.identifier(pos, compiler) } def isLineBreak(idx: Int) = if (idx >= length) false else content(idx) match { // don't identify the CR in CR LF as a line break, since LF will do. case CR => (idx + 1 == length) || (content(idx + 1) != LF) case x => isLineBreakChar(x) } private lazy val lineIndices: Array[Int] = { val buf = new ArrayBuffer[Int] buf += 0 for (i <- 0 until content.length) if (isLineBreak(i)) buf += i + 1 buf += content.length // sentinel, so that findLine below works smoother buf.toArray } def lineToOffset(index : Int): Int = lineIndices(index) private var lastLine = 0 /** Convert offset to line in this source file * Lines are numbered from 0 */ def offsetToLine(offset: Int): Int = { val lines = lineIndices def findLine(lo: Int, hi: Int, mid: Int): Int = if (offset < lines(mid)) findLine(lo, mid - 1, (lo + mid - 1) / 2) else if (offset >= lines(mid + 1)) findLine(mid + 1, hi, (mid + 1 + hi) / 2) else mid lastLine = findLine(0, lines.length, lastLine) lastLine } /** // An array which maps line numbers (counting from 0) to char offset into content private lazy val lineIndices: Array[Int] = { val xs = content.indices filter isLineBreak map (_ + 1) toArray val arr = new Array[Int](xs.length + 1) arr(0) = 0 System.arraycopy(xs, 0, arr, 1, xs.length) arr } // A reverse map which also hunts down the right answer on non-exact lookups private class SparseReverser() { val revMap = Map(lineIndices.zipWithIndex: _*) def apply(x: Int): Int = revMap.get(x) match { case Some(res) => res case _ => var candidate = x - 1 while (!revMap.contains(candidate)) candidate -= 1 revMap(candidate) } } private lazy val lineIndicesRev = new SparseReverser() def lineToOffset(index : Int): Int = lineIndices(index) def offsetToLine(offset: Int): Int = lineIndicesRev(offset) */ } /** A source file composed of multiple other source files. * * @version 1.0 */ class CompoundSourceFile( name: String, components: List[BatchSourceFile], contents: Array[Char]) extends BatchSourceFile(name, contents) { /** The usual constructor. Specify a name for the compound file and * a list of component sources. */ def this(name: String, components: BatchSourceFile*) = { this( name, components.toList, Array.concat(components.toList.map(comp => CompoundSourceFile.stripSU(comp.content).toArray):_*)) } /** Create an instance with the specified components and a generic name. */ def this(components: BatchSourceFile*) = this("(virtual file)", components.toList:_*) override def positionInUltimateSource(position: Position) = { if (position.offset.isEmpty) super.positionInUltimateSource(position) else { println("!!!") var off = position.offset.get var compsLeft = components // the search here has to be against the length of the files underlying the // components, not their advertised length (which in the case of a fragment is // less than the underlying length.) Otherwise we can and will overshoot the // correct component and return a garbage position. while (compsLeft.head.underlyingLength-1 <= off && !compsLeft.tail.isEmpty) { println("discarding "+compsLeft.head) off = off - compsLeft.head.underlyingLength + 1 compsLeft = compsLeft.tail } // now that we've identified the correct component, we have to adjust the // position we report since it is expected relative to the fragment, not the // underlying file. Thus, off - comp.start. val comp = compsLeft.head comp.positionInUltimateSource(new OffsetPosition(this, off - comp.start)) } } } object CompoundSourceFile { private[util] def stripSU(chars: Array[Char]) = if (chars.length > 0 && chars.last == SourceFile.SU) chars.slice(0, chars.length-1) else chars } /** One portion of an underlying file. The fragment includes * the indices from the specified start (inclusively) to stop * (not inclusively). */ class SourceFileFragment private ( name: String, underlyingFile: BatchSourceFile, override val start: Int, stop: Int, contents: Array[Char]) extends BatchSourceFile(name, contents) { override def underlyingLength = underlyingFile.length def this(name: String, underlyingFile: BatchSourceFile, start: Int, stop: Int) = this( name, underlyingFile, start, stop, { assert(start >= 0) assert(start <= stop) assert(start <= underlyingFile.length) assert(stop <= underlyingFile.length) underlyingFile.content.slice(start, stop).toArray }) def this(underlyingFile: BatchSourceFile, start: Int, stop: Int) = this( "(fragment of " + underlyingFile.file.name + ")", underlyingFile, start, stop) override def positionInUltimateSource(position: Position) = { if (position.offset.isEmpty) super.positionInUltimateSource(position) else { super.positionInUltimateSource( new OffsetPosition(this, position.offset.get)) } } }