Skip to content

Commit

Permalink
Added context extraction support for Lists and their items.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsalathe committed Sep 6, 2023
1 parent aea4702 commit 2d52269
Show file tree
Hide file tree
Showing 5 changed files with 182 additions and 45 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 5 additions & 4 deletions src/main/scala/io/smartdatalake/context/SDLBContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))

Expand All @@ -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?
Expand All @@ -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)

}

Expand Down
75 changes: 67 additions & 8 deletions src/main/scala/io/smartdatalake/context/hocon/HoconParser.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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._

Expand All @@ -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(".")
Expand All @@ -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)
Expand Down Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Loading

0 comments on commit 2d52269

Please sign in to comment.