diff --git a/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala b/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala index a85734e..f2c74b4 100644 --- a/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala +++ b/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala @@ -9,7 +9,7 @@ import scala.util.{Failure, Success, Try} class SDLBCompletionEngineImpl(private val schemaReader: SchemaReader) extends SDLBCompletionEngine { - override def generateCompletionItems(context: SDLBContext): List[CompletionItem] = context.parentPath match + override def generateCompletionItems(context: SDLBContext): List[CompletionItem] = context.parentPath match //TODO split path by '.' and handle logic if element list or not like that? Carry over the new config like that too? case path if path.startsWith("actions") && path.count(_ == '.') == 1 => generatePropertiesOfAction(context) case "actions" => generateTemplatesForAction() case path if path.startsWith("actions") => List.empty[CompletionItem] //TODO when going deeper find a good recursive approach and mb merge it with first case @@ -37,7 +37,7 @@ class SDLBCompletionEngineImpl(private val schemaReader: SchemaReader) extends S private def generatePropertiesOfAction(context: SDLBContext): List[CompletionItem] = def isMissingInConfig(item: SchemaItem): Boolean = Try(context.textContext.config.getAnyRef(context.parentPath + "." + item.name)).isFailure - val tActionType: Try[String] = Try(context.textContext.config.getString(context.parentPath + ".type")) + val tActionType: Try[String] = Try(context.textContext.config.getString(context.parentPath + ".type")) // In list, it looks like fixture.config.getList("actions.select-airport-cols.transformers").get(0).asInstanceOf[ConfigObject].get("type").unwrapped() tActionType match case Success(actionType) => schemaReader.retrieveActionProperties(actionType).filter(isMissingInConfig).map(createCompletionItem).toList case Failure(_) => typeList diff --git a/src/main/scala/io/smartdatalake/context/SDLBContext.scala b/src/main/scala/io/smartdatalake/context/SDLBContext.scala index 12be5cb..3e04bba 100644 --- a/src/main/scala/io/smartdatalake/context/SDLBContext.scala +++ b/src/main/scala/io/smartdatalake/context/SDLBContext.scala @@ -6,7 +6,7 @@ import io.smartdatalake.context.TextContext.EMPTY_TEXT_CONTEXT import io.smartdatalake.context.hocon.HoconParser import io.smartdatalake.utils.MultiLineTransformer -case class SDLBContext private(textContext: TextContext, parentPath: String, parentWord: String, word: String) { +case class SDLBContext private(textContext: TextContext, parentPath: String, parentWord: String, word: String, oIndex: Option[Int]) { def withText(newText: String): SDLBContext = copy(textContext = textContext.update(newText)) @@ -17,7 +17,8 @@ case class SDLBContext private(textContext: TextContext, parentPath: String, par val word = HoconParser.retrieveWordAtPosition(configText, newLine, newCol) val (parentLine, parentWord) = HoconParser.retrieveDirectParent(configText, newLine, newCol) val path = HoconParser.retrievePath(config, parentLine) - copy(parentPath = path, parentWord = parentWord, word = word) + val oIndex = HoconParser.findIndexIfInList(configText, newLine, newCol) + copy(parentPath = path, parentWord = parentWord, word = word, oIndex = oIndex) //TODO keep that method? @@ -27,9 +28,9 @@ case class SDLBContext private(textContext: TextContext, parentPath: String, par } object SDLBContext { - val EMPTY_CONTEXT: SDLBContext = SDLBContext(EMPTY_TEXT_CONTEXT, "", "", "") + val EMPTY_CONTEXT: SDLBContext = SDLBContext(EMPTY_TEXT_CONTEXT, "", "", "", None) - def fromText(originalText: String): SDLBContext = SDLBContext(TextContext.create(originalText), "", "", "") + def fromText(originalText: String): SDLBContext = SDLBContext(TextContext.create(originalText), "", "", "", None) } diff --git a/src/main/scala/io/smartdatalake/context/hocon/HoconParser.scala b/src/main/scala/io/smartdatalake/context/hocon/HoconParser.scala index 5c65e86..ede94aa 100644 --- a/src/main/scala/io/smartdatalake/context/hocon/HoconParser.scala +++ b/src/main/scala/io/smartdatalake/context/hocon/HoconParser.scala @@ -1,11 +1,14 @@ package io.smartdatalake.context.hocon -import com.typesafe.config.{Config, ConfigException, ConfigFactory, ConfigObject} +import com.typesafe.config.{Config, ConfigException, ConfigFactory, ConfigList, ConfigObject, ConfigValue} import io.smartdatalake.utils.MultiLineTransformer.{computeCorrectedPosition, flattenMultiLines} +import java.util.Map.Entry as JEntry + import scala.annotation.tailrec -import scala.util.{Try, Success, Failure} -import io.smartdatalake.context.hocon.{HoconTokens => Token} +import scala.util.{Failure, Success, Try} +import io.smartdatalake.context.hocon.HoconTokens as Token +import io.smartdatalake.conversions.ScalaJavaConverterAPI.* /** * Utility class to parse HOCON-formatted files @@ -33,6 +36,13 @@ private[context] object HoconParser: * @return path in format "a.b.c" */ def retrievePath(config: Config, line: Int): String = + def matchTypeValueAndSearchRecursive(key: String, configValue: ConfigValue, currentPath: String): Option[String] = { + configValue match + case configList: ConfigList => configList.toScala.zipWithIndex.flatMap{ (config, index) => matchTypeValueAndSearchRecursive(index.toString, config, currentPath + "." + key)}.headOption + case configObject: ConfigObject => searchPath(configObject, currentPath + "." + key) + case _ => None + } + def searchPath(currentConfig: ConfigObject, currentPath: String): Option[String] = import scala.jdk.CollectionConverters._ @@ -42,10 +52,7 @@ private[context] object HoconParser: case Some(entry) => Some(currentPath + "." + entry.getKey) case None => - entrySet.flatMap { entry => // small caviat: still continue to look at the neighbor's level of the parent of the endpoint. - entry.getValue match - case configObject: ConfigObject => searchPath(configObject, currentPath + "." + entry.getKey) - case _ => None + entrySet.flatMap { entry => matchTypeValueAndSearchRecursive(entry.getKey, entry.getValue, currentPath) }.headOption searchPath(config.root(), "").getOrElse("").stripPrefix(".") @@ -71,7 +78,7 @@ private[context] object HoconParser: (0, "") else val textLine = text.split(Token.NEW_LINE)(line-1).filterNot(c => c.isWhitespace || c == Token.START_LIST || c == Token.END_LIST).takeWhile(_ != Token.COMMENT).mkString - val newDepth = depth - textLine.count(_ == Token.START_OBJECT) + textLine.count(_ == Token.END_OBJECT) - textLine.count(_ == Token.END_LIST) + textLine.count(_ == Token.START_LIST) //TODO handle list better + val newDepth = depth - textLine.count(_ == Token.START_OBJECT) + textLine.count(_ == Token.END_OBJECT) - textLine.count(_ == Token.END_LIST) + textLine.count(_ == Token.START_LIST) if textLine.contains(Token.END_OBJECT) then retrieveHelper(line-1, newDepth) else textLine.split(Token.KEY_VAL_SPLIT_REGEX) match case Array(singleBlock) => if singleBlock.isBlank then retrieveHelper(line-1, newDepth) else if depth == 0 then (line, singleBlock) else retrieveHelper(line-1, newDepth) @@ -109,3 +116,55 @@ private[context] object HoconParser: case _ => getWordAtIndex(textLine, column) + + def findIndexIfInList(text: String, line: Int, column: Int): Option[Int] = + val absolutePosition = lineColToAbsolutePosition(text, line, column) + findListAreaFrom(text, absolutePosition) match + case None => None + case Some((startPosition, endPosition)) => + @tailrec + def buildObjectPositions(currentPosition: Int, currentList: List[(Int, Int)]): List[(Int, Int)] = //TODO unit test? + val nextStartObjectTokenRelativePosition = text.substring(currentPosition).indexOf(Token.START_OBJECT) + if nextStartObjectTokenRelativePosition == -1 then + currentList + else + val nextStartObjectTokenAbsolutePosition = nextStartObjectTokenRelativePosition + currentPosition + if nextStartObjectTokenAbsolutePosition > endPosition then + currentList + else + findObjectAreaFrom(text, nextStartObjectTokenAbsolutePosition + 1) match // +1 to enter in the object + case None => currentList + case Some((start, end)) => buildObjectPositions(end + 1, (start, end)::currentList) + + val objectPositions = buildObjectPositions(startPosition, List.empty[(Int, Int)]).reverse + objectPositions + .zipWithIndex + .find{(bounds, _) => absolutePosition >= bounds(0) && absolutePosition <= bounds(1)} + .map(_._2) + + + + + + private[hocon] def findObjectAreaFrom(text: String, position: Int): Option[(Int, Int)] = findAreaFrom(text, position, Token.START_OBJECT, Token.END_OBJECT) + private[hocon] def findListAreaFrom(text: String, position: Int): Option[(Int, Int)] = findAreaFrom(text, position, Token.START_LIST, Token.END_LIST) + + private def findAreaFrom(text: String, position: Int, startToken: Char, endToken: Char): Option[(Int, Int)] = + @tailrec + def indexOfWithDepth(char: Char, oppositeChar: Char, position: Int, direction: 1 | -1, depth: Int=0): Int = + assert(depth >= 0) + if position < 0 || position == text.length then -1 else text(position) match + case c if c == char && depth == 0 => position + case c if c == char => indexOfWithDepth(char, oppositeChar, position + direction, direction, depth - 1) + case c if c == oppositeChar => indexOfWithDepth(char, oppositeChar, position + direction, direction, depth + 1) + case _ => indexOfWithDepth(char, oppositeChar, position + direction, direction, depth) + + val startPosition = indexOfWithDepth(startToken, endToken, position-1, -1) + val endPosition = indexOfWithDepth(endToken, startToken, position, 1) + if startPosition != -1 && endPosition != -1 then Some((startPosition+1, endPosition)) else None // (...+1, ...-1) to exclude list characters themselves + + private[hocon] def lineColToAbsolutePosition(text: String, line: Int, column: Int): Int = + val textLine = text.split(Token.NEW_LINE) + val nCharactersBeforeCurrentLine = textLine.take(line-1).map(line => line.length + 1).sum // +1 for \n character + val nCharactersCurrentLine = math.min(textLine(line-1).length, column) + nCharactersCurrentLine + nCharactersBeforeCurrentLine \ No newline at end of file diff --git a/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala b/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala index d395e5a..1d972a6 100644 --- a/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala +++ b/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala @@ -1,16 +1,22 @@ package io.smartdatalake.conversions +import com.typesafe.config.{ConfigList, ConfigValue} + import java.util.concurrent.CompletableFuture import org.eclipse.lsp4j.jsonrpc.messages + import scala.concurrent.Future import scala.jdk.FutureConverters.* import scala.jdk.CollectionConverters.* +import java.util.List as JList trait ScalaJavaConverter { extension [T] (f: Future[T]) def toJava: CompletableFuture[T] = f.asJava.toCompletableFuture - extension [T] (l: List[T]) def toJava: java.util.List[T] = l.asJava + extension [T] (l: List[T]) def toJava: JList[T] = l.asJava + + extension [T] (l: JList[T]) def toScala: List[T] = l.asScala.toList extension [L, R] (either: Either[L, R]) def toJava: messages.Either[L, R] = either match case Left(leftValue) => messages.Either.forLeft(leftValue) diff --git a/src/test/scala/io/smartdatalake/context/hocon/HoconParserSpec.scala b/src/test/scala/io/smartdatalake/context/hocon/HoconParserSpec.scala index 0e2ec6f..087243f 100644 --- a/src/test/scala/io/smartdatalake/context/hocon/HoconParserSpec.scala +++ b/src/test/scala/io/smartdatalake/context/hocon/HoconParserSpec.scala @@ -11,7 +11,7 @@ import scala.util.Using class HoconParserSpec extends UnitSpec { val (leftCol, rightCol) = (0, 999) - case class CaretData(line: Int, column: Int, parentLine: Int, parentName: String, path: String) + case class CaretData(line: Int, column: Int, parentLine: Int, parentName: String, path: String, oIndex: Option[Int]=None) case class Fixture(originalText: String, text: String, config: Config) "Hocon parser" should "find path in hocon file" in { @@ -144,7 +144,7 @@ class HoconParserSpec extends UnitSpec { } - it should "find path in file with lists" in { //TODO not correct yet + it should "find path in file with lists" in { //TODO test nested lists val fixture = loadFixture("fixture/hocon/with-lists-example.conf") @@ -155,9 +155,9 @@ class HoconParserSpec extends UnitSpec { CaretData(4, leftCol, 2, "select-airport-cols", "actions.select-airport-cols"), CaretData(5, leftCol, 2, "select-airport-cols", "actions.select-airport-cols"), CaretData(6, leftCol, 2, "select-airport-cols", "actions.select-airport-cols"), - CaretData(7, leftCol, 6, "transformers", "actions.select-airport-cols.transformers"), - CaretData(8, leftCol, 6, "transformers", "actions.select-airport-cols.transformers"), - CaretData(9, leftCol, 6, "transformers", "actions.select-airport-cols.transformers"), + CaretData(7, leftCol, 6, "transformers", "actions.select-airport-cols.transformers", Some(0)), + CaretData(8, leftCol, 6, "transformers", "actions.select-airport-cols.transformers", Some(0)), + CaretData(9, leftCol, 6, "transformers", "actions.select-airport-cols.transformers", Some(0)), CaretData(10, leftCol, 2, "select-airport-cols", "actions.select-airport-cols"), CaretData(11, leftCol, 10, "metadata", "actions.select-airport-cols.metadata"), CaretData(12, leftCol, 10, "metadata", "actions.select-airport-cols.metadata"), @@ -168,25 +168,24 @@ class HoconParserSpec extends UnitSpec { CaretData(17, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(18, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(19, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), - CaretData(20, leftCol, 19, "transformers", "actions.join-departures-airports.transformers"), - CaretData(21, leftCol, 19, "transformers", "actions.join-departures-airports.transformers"), - CaretData(22, leftCol, 21, "code", ""), //TODO investigate why code is not found. That might help understanding how lists are generally handled by HOCON - CaretData(23, leftCol, 21, "code", ""), + CaretData(20, leftCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(0)), + CaretData(21, leftCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(0)), + CaretData(22, leftCol, 21, "code", "actions.join-departures-airports.transformers.0.code", Some(0)), + CaretData(23, leftCol, 21, "code", "actions.join-departures-airports.transformers.0.code", Some(0)), CaretData(24, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), - CaretData(25, leftCol, 19, "transformers", "actions.join-departures-airports.transformers"), - CaretData(26, leftCol, 19, "transformers", "actions.join-departures-airports.transformers"), - CaretData(27, leftCol, 26, "code", ""), - CaretData(28, leftCol, 26, "code", ""), - CaretData(29, leftCol, 19, "transformers", "actions.join-departures-airports.transformers"), + CaretData(25, leftCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(1)), + CaretData(26, leftCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(1)), + CaretData(27, leftCol, 26, "code", "actions.join-departures-airports.transformers.1.code", Some(1)), + CaretData(28, leftCol, 26, "code", "actions.join-departures-airports.transformers.1.code", Some(1)), + CaretData(29, leftCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(1)), CaretData(30, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(31, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(32, leftCol, 31, "metadata", "actions.join-departures-airports.metadata"), CaretData(33, leftCol, 31, "metadata", "actions.join-departures-airports.metadata"), CaretData(34, leftCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(35, leftCol, 1, "actions", "actions") - ) // fixture.config.getValue("actions.join-departures-airports.transformers").asInstanceOf[com.typesafe.config.ConfigList].get(0).origin() == 19 - // fixture.config.getValue("actions.join-departures-airports.transformers").asInstanceOf[com.typesafe.config.ConfigList].get(0).asInstanceOf[com.typesafe.config.ConfigObject].toConfig.getValue("code").origin() == 21 - + ) + validateText(fixture, leftCol, leftCaretData) val rightCaretData = List( @@ -195,9 +194,9 @@ class HoconParserSpec extends UnitSpec { CaretData(3, rightCol, 3, "type", "actions.select-airport-cols.type"), CaretData(4, rightCol, 4, "inputId", "actions.select-airport-cols.inputId"), CaretData(5, rightCol, 5, "outputId", "actions.select-airport-cols.outputId"), - CaretData(6, rightCol, 6, "transformers", "actions.select-airport-cols.transformers"), - CaretData(7, rightCol, 7, "type", ""), - CaretData(8, rightCol, 8, "code", ""), + CaretData(6, rightCol, 6, "transformers", "actions.select-airport-cols.transformers", Some(0)), + CaretData(7, rightCol, 7, "type", "actions.select-airport-cols.transformers.0.type", Some(0)), + CaretData(8, rightCol, 8, "code", "actions.select-airport-cols.transformers.0.code", Some(0)), CaretData(9, rightCol, 2, "select-airport-cols", "actions.select-airport-cols"), CaretData(10, rightCol, 10, "metadata", "actions.select-airport-cols.metadata"), CaretData(11, rightCol, 11, "feed", "actions.select-airport-cols.metadata.feed"), @@ -208,16 +207,16 @@ class HoconParserSpec extends UnitSpec { CaretData(16, rightCol, 16, "type", "actions.join-departures-airports.type"), CaretData(17, rightCol, 17, "inputIds", "actions.join-departures-airports.inputIds"), CaretData(18, rightCol, 18, "outputIds", "actions.join-departures-airports.outputIds"), - CaretData(19, rightCol, 19, "transformers", "actions.join-departures-airports.transformers"), - CaretData(20, rightCol, 20, "type", ""), - CaretData(21, rightCol, 21, "code", ""), - CaretData(22, rightCol, 22, "btl-connected-airports", ""), + CaretData(19, rightCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(0)), + CaretData(20, rightCol, 20, "type", "actions.join-departures-airports.transformers.0.type", Some(0)), + CaretData(21, rightCol, 21, "code", "actions.join-departures-airports.transformers.0.code", Some(0)), + CaretData(22, rightCol, 22, "btl-connected-airports", "actions.join-departures-airports.transformers.0.code.btl-connected-airports", Some(0)), CaretData(23, rightCol, 19, "transformers", "actions.join-departures-airports.transformers"), - CaretData(24, rightCol, 0, "", ""), - CaretData(25, rightCol, 25, "type", ""), - CaretData(26, rightCol, 26, "code", ""), - CaretData(27, rightCol, 27, "btl-departures-arrivals-airports", ""), - CaretData(28, rightCol, 19, "transformers", "actions.join-departures-airports.transformers"), + CaretData(24, rightCol, 0, "", "", Some(1)), + CaretData(25, rightCol, 25, "type", "actions.join-departures-airports.transformers.1.type", Some(1)), + CaretData(26, rightCol, 26, "code", "actions.join-departures-airports.transformers.1.code", Some(1)), + CaretData(27, rightCol, 27, "btl-departures-arrivals-airports", "actions.join-departures-airports.transformers.1.code.btl-departures-arrivals-airports", Some(1)), + CaretData(28, rightCol, 19, "transformers", "actions.join-departures-airports.transformers", Some(1)), CaretData(29, rightCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(30, rightCol, 15, "join-departures-airports", "actions.join-departures-airports"), CaretData(31, rightCol, 31, "metadata", "actions.join-departures-airports.metadata"), @@ -271,6 +270,77 @@ class HoconParserSpec extends UnitSpec { } + it should "transform line column position to absolute position" in { + val fixture = loadFixture("fixture/hocon/with-lists-example.conf") + val text = fixture.text + + HoconParser.lineColToAbsolutePosition(text, 1, 4) shouldBe 4 + HoconParser.lineColToAbsolutePosition(text, 1, 999) shouldBe 9 + HoconParser.lineColToAbsolutePosition(text, 3, 4) shouldBe 38 + HoconParser.lineColToAbsolutePosition(text, 3, 999) shouldBe 55 + HoconParser.lineColToAbsolutePosition(text, 17, 16) shouldBe 376 + HoconParser.lineColToAbsolutePosition(text, 17, 43) shouldBe 403 + HoconParser.lineColToAbsolutePosition(text, 19, 20) shouldBe 477 + HoconParser.lineColToAbsolutePosition(text, 30, 4) shouldBe 715 + } + + it should "find list areas with simple cases" in { + val simpleText = "[]" + + HoconParser.findListAreaFrom(simpleText, 0) shouldBe None + HoconParser.findListAreaFrom(simpleText, 1) shouldBe Some((1, 1)) + HoconParser.findListAreaFrom(simpleText, 2) shouldBe None + + val mediumText = "[[]]" + HoconParser.findListAreaFrom(mediumText, 0) shouldBe None + HoconParser.findListAreaFrom(mediumText, 1) shouldBe Some((1, 3)) + HoconParser.findListAreaFrom(mediumText, 2) shouldBe Some((2, 2)) + HoconParser.findListAreaFrom(mediumText, 3) shouldBe Some((1, 3)) + HoconParser.findListAreaFrom(mediumText, 4) shouldBe None + + val harderText = "[[ ]][ ]" + HoconParser.findListAreaFrom(harderText, 0) shouldBe None + HoconParser.findListAreaFrom(harderText, 1) shouldBe Some((1, 4)) + HoconParser.findListAreaFrom(harderText, 2) shouldBe Some((2, 3)) + HoconParser.findListAreaFrom(harderText, 3) shouldBe Some((2, 3)) + HoconParser.findListAreaFrom(harderText, 4) shouldBe Some((1, 4)) + HoconParser.findListAreaFrom(harderText, 5) shouldBe None + HoconParser.findListAreaFrom(harderText, 6) shouldBe Some((6, 9)) + HoconParser.findListAreaFrom(harderText, 7) shouldBe Some((6, 9)) + HoconParser.findListAreaFrom(harderText, 8) shouldBe Some((6, 9)) + HoconParser.findListAreaFrom(harderText, 9) shouldBe Some((6, 9)) + HoconParser.findListAreaFrom(harderText, 10) shouldBe None + } + + it should "find list areas with fixture" in { + val fixture = loadFixture("fixture/hocon/with-lists-example.conf") + val text = fixture.text + + + HoconParser.findListAreaFrom(text, 376) shouldBe Some((376, 404)) // 376 == (17, 16), 403 == (17, 43) + HoconParser.findListAreaFrom(text, 375) shouldBe None + HoconParser.findListAreaFrom(text, 405) shouldBe None + + HoconParser.findListAreaFrom(text, 516) shouldBe Some((477, 715)) // 516 == (21, 6) + + HoconParser.findListAreaFrom(text, 38) shouldBe None + HoconParser.findListAreaFrom(text, 716) shouldBe None + + } + + it should "find index of currentList" in { + val fixture = loadFixture("fixture/hocon/with-lists-example.conf") + val text = fixture.text + + HoconParser.findIndexIfInList(text, 1, 0) shouldBe None + HoconParser.findIndexIfInList(text, 17, 20) shouldBe None + HoconParser.findIndexIfInList(text, 22, 0) shouldBe Some(0) + HoconParser.findIndexIfInList(text, 23, 8) shouldBe None + HoconParser.findIndexIfInList(text, 27, 0) shouldBe Some(1) + HoconParser.findIndexIfInList(text, 32, 0) shouldBe None + + } + private def validateText(fixture: Fixture, column: Int, caretDataList: List[CaretData], positionMap: Option[List[(Int, Int)]]=None): Unit = val totalLines = fixture.originalText.count(_ == '\n') + 1 @@ -279,7 +349,8 @@ class HoconParserSpec extends UnitSpec { val columnNumber = positionMap.map(pm => pm(i-1)(1) + column).getOrElse(column) val (line, word) = HoconParser.retrieveDirectParent(fixture.text, lineNumber, columnNumber) val path = HoconParser.retrievePath(fixture.config, line) - val caretData = CaretData(lineNumber, columnNumber, line, word, path) + val oIndex = HoconParser.findIndexIfInList(fixture.text, lineNumber, columnNumber) + val caretData = CaretData(lineNumber, columnNumber, line, word, path, oIndex) caretData should be(caretDataList(i - 1))