Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/src/main/scala/org/scastie/api/CompilerInfo.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ object Problem {
implicit val problemDecoder: Decoder[Problem] = deriveDecoder[Problem]
}

case class Problem(severity: Severity, line: Option[Int], message: String)
case class Problem(severity: Severity, line: Option[Int], startColumn: Option[Int], endColumn: Option[Int], message: String)
Original file line number Diff line number Diff line change
Expand Up @@ -467,7 +467,7 @@ case class ScastieState(

def clearSnippetId: ScastieState = copyAndSave(snippetId = None)

private def info(message: String) = Problem(Info, None, message)
private def info(message: String) = Problem(Info, None, None, None, message)

def setForcedProgramMode(forcedProgramMode: Boolean): ScastieState = {
if (!forcedProgramMode) this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,12 +114,32 @@ object CodeEditor {

private def getDecorations(props: CodeEditor, doc: Text): js.Array[Diagnostic] = {
val errors = props.compilationInfos
.filter(prob => prob.line.isDefined && prob.line.get <= doc.lines)
.filter(prob => prob.line.isDefined)
.map(problem => {
val line = problem.line.get max 1
val maxLine = doc.lines.toInt
val line = problem.line.get.max(1).min(maxLine)
val lineInfo = doc.line(line)
val lineLength = lineInfo.length.toInt

val (startColumn, endColumn) =
if (problem.line.get > maxLine) {
val endPos = lineInfo.to
(endPos, endPos)
} else {
(problem.startColumn, problem.endColumn) match {
case (Some(start), Some(end)) if start > 0 && end >= start =>
val clampedStart = (start min (lineLength + 1)) max 1
val clampedEnd = (end min (lineLength + 1)) max clampedStart
(lineInfo.from + clampedStart - 1, lineInfo.from + clampedEnd - 1)
case (Some(start), _) if start > 0 =>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this ever happen? Maybe lets throw here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question 😅. I honestly can't remember what the use case for this was. I’m sure I had a reason, but from what I’ve tested now, I couldn’t reproduce this case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe lets make it a tuple Option[(Int, Int)] ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think Diagnostic requires a Double. Is this the solution you had in mind?

val preciseRangeOpt: Option[(Double, Double)] =
  if (problem.line.get > maxLine) {
    val endPos = lineInfo.to
    Some((endPos, endPos))
  } else {
    (problem.startColumn, problem.endColumn) match {
      case (Some(start), Some(end)) if start > 0 && end >= start =>
        val clampedStart = (start min (lineLength + 1)) max 1
        val clampedEnd = (end min (lineLength + 1)) max clampedStart
        Some((lineInfo.from + clampedStart - 1, lineInfo.from + clampedEnd - 1))
      case _ =>
        None
    }
  }

val (startColumn, endColumn) = preciseRangeOpt match {
  case Some((start, end)) =>
    (start, end)
  case None =>
    (lineInfo.from, lineInfo.to)
}

val clampedStart = (start min (lineLength + 1)) max 1
(lineInfo.from + clampedStart - 1, lineInfo.from + clampedStart)
case _ =>
(lineInfo.from, lineInfo.to)
}
}

Diagnostic(lineInfo.from, problem.message, parseSeverity(problem.severity), lineInfo.to)
Diagnostic(startColumn, problem.message, parseSeverity(problem.severity), endColumn)
.setRenderMessage(CallbackTo {
val wrapper = dom.document.createElement("pre")
wrapper.innerHTML = HTMLFormatter.format(problem.message)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,13 @@ object InstrumentationFailure {

case class InstrumentationSuccess(
instrumentedCode: String,
lineMapping: Int => Int
positionMapper: Option[PositionMapper] = None
)

object Instrument {
def getParsingLineOffset(isWorksheet: Boolean): Int = if (isWorksheet) -1 else 0
def getExceptionLineOffset(isWorksheet: Boolean): Int = if (isWorksheet) -2 else 0
def getMessageLineOffset(isWorksheet: Boolean, isScalaCli: Boolean): Int = (isWorksheet, isScalaCli) match {
case (true, _) => -2
case (false, true) => 1
case (false, false) => 0
}
def getMessageLineOffset(isWorksheet: Boolean): Int = if (isWorksheet) -2 else 0

import InstrumentationFailure._

Expand Down Expand Up @@ -213,9 +209,14 @@ object Instrument {
case target => scala(target.scalaVersion)
}

val offset = target match {
case _: ScalaCli => usingDirectives.length + prelude.length + 1
case _ => prelude.length + 1
val isScalaCli = target match {
case _: ScalaCli => true
case _ => false
}

val offset = isScalaCli match {
case true => usingDirectives.length + prelude.length + 1
case false => prelude.length + 1
}

maybeDialect match {
Expand All @@ -228,11 +229,11 @@ object Instrument {
if (!hasMainMethod(parsed.get)) {
val (instrumentedCode, entryPoint) = instrument(parsed.get, offset, isScalaJs)

val lineMapping = LineMapper(instrumentedCode)
val positionMapper = PositionMapper(instrumentedCode, isScalaCli)

Right(InstrumentationSuccess(
s"""$instrumentedCode\n$entryPoint""",
lineMapping
Some(positionMapper)
))
} else {
Left(HasMainMethod)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import org.scastie.api._
import scala.meta.inputs.Input
import scala.meta.parsers.Parsed

case class InstrumentationFailureReport(message: String, line: Option[Int]) {
case class InstrumentationFailureReport(message: String, line: Option[Int], startColumn: Option[Int] = None, endColumn: Option[Int] = None) {
def toProgress(snippetId: SnippetId): SnippetProgress = {
SnippetProgress.default.copy(
ts = Some(Instant.now.toEpochMilli),
snippetId = Some(snippetId),
compilationInfos = List(Problem(Error, line, message))
compilationInfos = List(Problem(Error, line, startColumn, endColumn, message))
)
}
}
Expand All @@ -22,16 +22,16 @@ object InstrumentedInputs {
def apply(inputs0: BaseInputs): Either[InstrumentationFailureReport, InstrumentedInputs] = {
if (inputs0.isWorksheetMode) {
val instrumented = Instrument(inputs0.code, inputs0.target).map {
case InstrumentationSuccess(instrumentedCode, lineMapper) =>
(inputs0.copyBaseInput(code = instrumentedCode), lineMapper)
case InstrumentationSuccess(instrumentedCode, positionMapper) =>
(inputs0.copyBaseInput(code = instrumentedCode), positionMapper)
}

instrumented match {
case Right((inputs, lineMapping)) => Right(
case Right((inputs, positionMapper)) => Right(
InstrumentedInputs(
inputs = inputs,
isForcedProgramMode = false,
lineMapping = lineMapping
positionMapper = positionMapper
)
)
case Left(error) =>
Expand All @@ -45,12 +45,20 @@ object InstrumentedInputs {
Left(InstrumentationFailureReport("This Scala target does not have a worksheet mode", None))

case ParsingError(error) =>
val lineOffset = Instrument.getParsingLineOffset(inputs0.isWorksheetMode)
val errorLine = (error.pos.startLine + lineOffset) max 1
val isScalaCli = inputs0.target match {
case _: ScalaCli => true
case _ => false
}
val positionMapper = PositionMapper(error.pos.input.text, isScalaCli)
val errorLine = positionMapper.mapLine(error.pos.startLine + 1) max 1
val errorStartCol = error.pos.startColumn + 1
val errorEndCol = error.pos.endColumn + 1

Right(InstrumentedInputs(
inputs = inputs0.copyBaseInput(code = error.pos.input.text),
isForcedProgramMode = false,
optionalParsingError = Some(InstrumentationFailureReport(error.message, Some(errorLine))),
optionalParsingError = Some(InstrumentationFailureReport(error.message, Some(errorLine), Some(errorStartCol), Some(errorEndCol))),
positionMapper = Some(positionMapper)
))

case InternalError(exception) =>
Expand All @@ -73,5 +81,5 @@ case class InstrumentedInputs(
inputs: BaseInputs,
isForcedProgramMode: Boolean,
optionalParsingError: Option[InstrumentationFailureReport] = None,
lineMapping: Int => Int = identity
positionMapper: Option[PositionMapper] = None
)

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
package org.scastie.instrumentation

import RuntimeConstants._
import org.scastie.api.ScalaTarget
import org.scastie.api.ScalaCli

case class PositionMapper private (
private val lineMapping: Int => Int,
private val columnOffsetMapping: Int => Int
) {

/**
* Maps a line from instrumented code back to the original user code.
*
* @param line
* the line number in the instrumented code (1-based)
* @return
* the corresponding line number in the original user code (1-based)
*/
def mapLine(line: Int): Int = {
lineMapping(line)
}

/**
* Maps a column from instrumented code back to the original user code, taking into account any column offsets
*
* @param line
* the line number in the instrumented code (1-based)
* @param column
* the column number in the instrumented code (1-based)
* @return
* the corresponding column number in the original user code (1-based)
*/
def mapColumn(line: Int, column: Int): Int = {
val offset = columnOffsetMapping(line)
math.max(1, column - offset)
}

}

object PositionMapper {

private val instrumentationPrefix: String = "val $t = "
private val prefixOffset: Int = instrumentationPrefix.length

/**
* Creates a PositionMapper that maps positions from instrumented Scastie code back to original code.
*
* @param instrumentedCode
* the code after Scastie instrumentation has been applied
* @param isScalaCli
* whether the target is Scala CLI
* @return
* a PositionMapper for mapping positions
*/
def apply(instrumentedCode: String, isScalaCli: Boolean = false): PositionMapper = {
val lines = instrumentedCode.split('\n')
val (lineMapping, columnOffsetMapping) = calculateMappings(lines, isScalaCli)
new PositionMapper(lineMapping, columnOffsetMapping)
}

private def calculateMappings(lines: Array[String], isScalaCli: Boolean): (Int => Int, Int => Int) = {
case class State(
userCodeLinesSeen: Int = 0,
lineMappings: Map[Int, Int] = Map.empty,
columnOffsets: Map[Int, Int] = Map.empty
)

val result = lines.zipWithIndex
.foldLeft(State()) { case (State(userCodeLinesSeen, lineMappings, columnOffsets), (line, index)) =>
val instrumentedLineNumber = index + 1
val trimmed = line.trim

val columnOffset = if (isWrappedUserCode(trimmed)) prefixOffset else 0

if (!isExperimentalImport(trimmed) && !isInstrumentationLine(trimmed, isScalaCli)) {
val newCount = userCodeLinesSeen + 1
State(
newCount,
lineMappings + (instrumentedLineNumber -> newCount),
columnOffsets + (instrumentedLineNumber -> columnOffset)
)
} else {
val fallbackLine = if (userCodeLinesSeen > 0) userCodeLinesSeen else 1
State(
userCodeLinesSeen,
lineMappings + (instrumentedLineNumber -> fallbackLine),
columnOffsets + (instrumentedLineNumber -> columnOffset)
)
}
}

val lineMapping: Int => Int =
instrumentedLineNumber => result.lineMappings.getOrElse(instrumentedLineNumber, instrumentedLineNumber)

val columnOffsetMapping: Int => Int =
instrumentedLineNumber => result.columnOffsets.getOrElse(instrumentedLineNumber, 0)

(lineMapping, columnOffsetMapping)
}

private def isInstrumentationLine(line: String, isScalaCli: Boolean): Boolean = {
line.matches("""\$doc\.startStatement\(\d+,\s*\d+\);""") ||
line.matches("""\$doc\.endStatement\(\);""") ||
line.matches("""\$doc\.binder\(.+,\s*\d+,\s*\d+\);""") ||
line == "scala.Predef.locally {" ||
line == "$t}" ||
line.startsWith(s"import $runtimePackage") ||
line.startsWith(s"object $instrumentedObject extends ScastieApp with $instrumentationRecorderT") ||
(line.startsWith("//> using") && isScalaCli)
}

private def isWrappedUserCode(line: String): Boolean = {
line.startsWith(instrumentationPrefix)
}

private def isExperimentalImport(line: String): Boolean = {
val experimentalRegex = """^\s*import\s+language\.experimental\.[^\n]+""".r
experimentalRegex.matches(line)
}

}
Loading
Loading