Skip to content

Commit

Permalink
Added hovering capabilities for properties of action types specifically.
Browse files Browse the repository at this point in the history
  • Loading branch information
dsalathe committed Aug 18, 2023
1 parent c76437c commit 4083f6f
Show file tree
Hide file tree
Showing 17 changed files with 186 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package io.smartdatalake.completion

import io.smartdatalake.completion.SDLBCompletionEngine
import io.smartdatalake.completion.schema.{ItemType, SchemaItem, SchemaReader, SchemaReaderImpl}
import io.smartdatalake.context.SDLBContext
import io.smartdatalake.schema.{ItemType, SchemaItem, SchemaReader, SchemaReaderImpl}
import org.eclipse.lsp4j.{CompletionItem, CompletionItemKind}

import scala.util.{Failure, Success, Try}
Expand Down
11 changes: 6 additions & 5 deletions src/main/scala/io/smartdatalake/context/SDLBContext.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,18 @@ 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) {
case class SDLBContext private(textContext: TextContext, parentPath: String, parentWord: String, word: String) {

def withText(newText: String): SDLBContext = copy(textContext = textContext.update(newText))

def withCaretPosition(originalLine: Int, originalCol: Int): SDLBContext =
val TextContext(originalText, configText, config) = textContext
if originalLine <= 0 || originalLine > originalText.count(_ == '\n') + 1 || originalCol < 0 then this else
val (newLine, newCol) = MultiLineTransformer.computeCorrectedPosition(originalText, originalLine, originalCol)
val (parentLine, word) = HoconParser.retrieveDirectParent(configText, newLine, newCol)
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 = word)
copy(parentPath = path, parentWord = parentWord, word = word)


//TODO keep that method?
Expand All @@ -26,9 +27,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, "", "", "")

def fromText(originalText: String): SDLBContext = SDLBContext(TextContext.create(originalText), "", "")
def fromText(originalText: String): SDLBContext = SDLBContext(TextContext.create(originalText), "", "", "")

}

Expand Down
23 changes: 22 additions & 1 deletion src/main/scala/io/smartdatalake/context/hocon/HoconParser.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.smartdatalake.utils.MultiLineTransformer.{computeCorrectedPosition, fl

import scala.annotation.tailrec
import scala.util.{Try, Success, Failure}
import io.smartdatalake.context.hocon.{HoconTokens => Token}

/**
* Utility class to parse HOCON-formatted files
Expand Down Expand Up @@ -60,7 +61,6 @@ private[context] object HoconParser:
* @return The direct parent
*/
def retrieveDirectParent(text: String, line: Int, column: Int): (Int, String) =
import io.smartdatalake.context.hocon.{HoconTokens => Token}

@tailrec
/*
Expand Down Expand Up @@ -88,3 +88,24 @@ private[context] object HoconParser:
else
retrieveHelper(line-1, 0)


def retrieveWordAtPosition(text: String, line: Int, col: Int): String =
def getWordAtIndex(textLine: String, index: Int): String =
val (leftPart, rightPart) = textLine.splitAt(index)
val leftPossibleIndex = leftPart.lastIndexOf(" ")
val leftIndex = if leftPossibleIndex == -1 then 0 else leftPossibleIndex
val rightPossibleIndex = rightPart.indexOf(" ")
val rightIndex = if rightPossibleIndex == -1 then textLine.length - 2 else index + rightPossibleIndex
textLine.substring(leftIndex, rightIndex).trim

val textLine = text.split(Token.NEW_LINE)(line-1) + " "
val column = math.min(textLine.length-1, col)
val leadingCharacter = textLine.charAt(column)
column match
case 0 if leadingCharacter.isWhitespace => ""
case _ if leadingCharacter.isWhitespace =>
val precedingCharacter = textLine.charAt(column-1)
if precedingCharacter.isWhitespace then "" else getWordAtIndex(textLine, column-1)

case _ => getWordAtIndex(textLine, column)

7 changes: 7 additions & 0 deletions src/main/scala/io/smartdatalake/hover/SDLBHoverEngine.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.smartdatalake.hover

import io.smartdatalake.context.SDLBContext
import org.eclipse.lsp4j.Hover

trait SDLBHoverEngine:
def generateHoveringInformation(context: SDLBContext): Hover
28 changes: 28 additions & 0 deletions src/main/scala/io/smartdatalake/hover/SDLBHoverEngineImpl.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.smartdatalake.hover
import io.smartdatalake.context.SDLBContext
import io.smartdatalake.schema.{SchemaReader, SchemaReaderImpl}
import org.eclipse.lsp4j.{Hover, MarkupContent, MarkupKind}

import scala.util.{Failure, Success, Try}

class SDLBHoverEngineImpl extends SDLBHoverEngine:

val schemaReader: SchemaReader = new SchemaReaderImpl("sdl-schema/sdl-schema-2.5.0.json") //TODO should be retrieved from a service keeping its state, object for example


override def generateHoveringInformation(context: SDLBContext): Hover =
val markupContent = new MarkupContent()
markupContent.setKind(MarkupKind.MARKDOWN)
markupContent.setValue(retrieveSchemaDescription(context))
new Hover(markupContent)

private def retrieveSchemaDescription(context: SDLBContext): String =
context.parentPath match
case path if path.startsWith("actions") && path.count(_ == '.') == 1 =>
val tActionType: Try[String] = Try(context.textContext.config.getString(context.parentPath + ".type"))
tActionType match
case Success(actionType) => schemaReader.retrieveActionPropertyDescription(actionType, context.word)
case Failure(_) => ""

case _ => ""

Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,15 @@ class SmartDataLakeLanguageServer extends LanguageServer with LanguageClientAwar
initializeResult.getCapabilities.setTextDocumentSync(TextDocumentSyncKind.Full)
val completionOptions = new CompletionOptions
initializeResult.getCapabilities.setCompletionProvider(completionOptions)

initializeResult.getCapabilities.setHoverProvider(true)

CompletableFuture.supplyAsync(() => initializeResult)
}

override def shutdown(): CompletableFuture[AnyRef] = {
errorCode = 0
null
CompletableFuture.completedFuture(null)
}

override def exit(): Unit = System.exit(errorCode)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package io.smartdatalake.languageserver

import io.smartdatalake.completion.SDLBCompletionEngineImpl
import io.smartdatalake.completion.{SDLBCompletionEngine, SDLBCompletionEngineImpl}
import io.smartdatalake.context.SDLBContext
import io.smartdatalake.hover.{SDLBHoverEngine, SDLBHoverEngineImpl}
import org.eclipse.lsp4j.jsonrpc.messages
import org.eclipse.lsp4j.services.TextDocumentService
import org.eclipse.lsp4j.{CodeAction, CodeActionParams, CodeLens, CodeLensParams, Command, CompletionItem, CompletionItemKind, CompletionList, CompletionParams, DefinitionParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, DocumentHighlight, DocumentHighlightParams, DocumentOnTypeFormattingParams, DocumentRangeFormattingParams, DocumentSymbol, DocumentSymbolParams, Hover, HoverParams, InsertReplaceEdit, Location, LocationLink, Position, Range, ReferenceParams, RenameParams, SignatureHelp, SignatureHelpParams, SymbolInformation, TextDocumentPositionParams, TextEdit, WorkspaceEdit}
import org.eclipse.lsp4j.{CodeAction, CodeActionParams, CodeLens, CodeLensParams, Command, CompletionItem, CompletionItemKind, CompletionList, CompletionParams, DefinitionParams, DidChangeTextDocumentParams, DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, DocumentFormattingParams, DocumentHighlight, DocumentHighlightParams, DocumentOnTypeFormattingParams, DocumentRangeFormattingParams, DocumentSymbol, DocumentSymbolParams, Hover, HoverParams, InsertReplaceEdit, Location, LocationLink, MarkupContent, MarkupKind, Position, Range, ReferenceParams, RenameParams, SignatureHelp, SignatureHelpParams, SymbolInformation, TextDocumentPositionParams, TextEdit, WorkspaceEdit}

import java.util
import java.util.concurrent.CompletableFuture
Expand All @@ -14,6 +15,8 @@ import scala.util.Using
class SmartDataLakeTextDocumentService extends TextDocumentService {

private var context: SDLBContext = SDLBContext.EMPTY_CONTEXT
private val completionEngine: SDLBCompletionEngine = new SDLBCompletionEngineImpl
private val hoverEngine: SDLBHoverEngine = new SDLBHoverEngineImpl //TODO DI!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

override def completion(params: CompletionParams): CompletableFuture[messages.Either[util.List[CompletionItem], CompletionList]] = {

Expand Down Expand Up @@ -47,7 +50,12 @@ class SmartDataLakeTextDocumentService extends TextDocumentService {

override def resolveCompletionItem(completionItem: CompletionItem): CompletableFuture[CompletionItem] = ???

override def hover(params: HoverParams): CompletableFuture[Hover] = super.hover(params) //TODO
override def hover(params: HoverParams): CompletableFuture[Hover] = {
CompletableFuture.supplyAsync(() => {
val hoverContext = context.withCaretPosition(params.getPosition.getLine + 1, params.getPosition.getCharacter)
hoverEngine.generateHoveringInformation(hoverContext)
})
}

override def signatureHelp(params: SignatureHelpParams): CompletableFuture[SignatureHelp] = super.signatureHelp(params)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smartdatalake.completion.schema
package io.smartdatalake.schema

enum ItemType(val name: String, val defaultValue: String) {
case STRING extends ItemType("string", "\"???\"")
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
package io.smartdatalake.completion.schema
package io.smartdatalake.schema

case class SchemaItem(name: String, itemType: ItemType, description: String, required: Boolean)
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.smartdatalake.completion.schema
package io.smartdatalake.schema

trait SchemaReader:
def retrieveActionProperties(typeName: String): Iterable[SchemaItem]

def retrieveActionPropertyDescription(typeName: String, propertyName: String): String

def retrieveActionTypesWithRequiredAttributes(): Iterable[(String, Iterable[SchemaItem])]
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package io.smartdatalake.completion.schema
package io.smartdatalake.schema

import scala.io.Source
import scala.util.Using
Expand All @@ -9,6 +9,12 @@ class SchemaReaderImpl(val schemaPath: String) extends SchemaReader {
Source.fromInputStream(inputStream).getLines().mkString("\n").trim
})


override def retrieveActionPropertyDescription(typeName: String, propertyName: String): String =
schema("definitions")("Action").obj.get(typeName)
.flatMap(typeContext => typeContext("properties").obj.get(propertyName))
.flatMap(property => property.obj.get("description").map(_.str)).getOrElse("")

override def retrieveActionProperties(typeName: String): Iterable[SchemaItem] =
schema("definitions")("Action").obj.get(typeName) match
case None => Iterable.empty[SchemaItem]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.smartdatalake.completion.schema

import io.smartdatalake.UnitSpec
import io.smartdatalake.schema.{ItemType, SchemaItem, SchemaReaderImpl}
import ujson.*

import scala.io.Source
Expand Down Expand Up @@ -58,5 +59,13 @@ class SchemaReaderSpec extends UnitSpec {

}

it should "retrieve some action property description" in {
val inputIdDescriptionOfCopyAction = schemaReader.retrieveActionPropertyDescription("CopyAction", "inputId")
inputIdDescriptionOfCopyAction shouldBe "inputs DataObject"

val overwriteDescriptionOfFileTransferAction = schemaReader.retrieveActionPropertyDescription("FileTransferAction", "overwrite")
overwriteDescriptionOfFileTransferAction shouldBe "Allow existing output file to be overwritten. If false the action will fail if a file to be created already exists. Default is true."
}


}
6 changes: 6 additions & 0 deletions src/test/scala/io/smartdatalake/context/SDLBContextSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -36,31 +36,37 @@ class SDLBContextSpec extends UnitSpec {
val line1Start = SDLBContext.fromText(text).withCaretPosition(1, 0)
line1Start.parentPath shouldBe ""
line1Start.parentWord shouldBe ""
line1Start.word shouldBe "global"
line1Start.getParentContext shouldBe None

val line1End = SDLBContext.fromText(text).withCaretPosition(1, 999)
line1End.parentPath shouldBe "global"
line1End.parentWord shouldBe "global"
line1End.word shouldBe "{"
line1End.getParentContext shouldBe defined

val line3Start = SDLBContext.fromText(text).withCaretPosition(3, 0)
line3Start.parentPath shouldBe "global.spark-options"
line3Start.parentWord shouldBe "spark-options"
line3Start.word shouldBe ""
line3Start.getParentContext.get.unwrapped().asInstanceOf[java.util.HashMap[String, Int]].get("spark.sql.shuffle.partitions") shouldBe 2

val line3End = SDLBContext.fromText(text).withCaretPosition(3, 999)
line3End.parentPath shouldBe "global.spark-options.spark.sql.shuffle.partitions"
line3End.parentWord shouldBe "\"spark.sql.shuffle.partitions\""
line3End.word shouldBe "2"
//line3End.getParentContext shouldBe defined //TODO this one is a problem because of the key with dots

val line5Start = SDLBContext.fromText(text).withCaretPosition(5, 0)
line5Start.parentPath shouldBe "global"
line5Start.parentWord shouldBe "global"
line5Start.word shouldBe "}"
line5Start.getParentContext shouldBe defined

val line5End = SDLBContext.fromText(text).withCaretPosition(5, 1)
line5End.parentPath shouldBe ""
line5End.parentWord shouldBe ""
line5End.word shouldBe "}"
line5End.getParentContext shouldBe None

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,38 @@ class HoconParserSpec extends UnitSpec {
it should "parse silently incorrect files" in {
HoconParser.parse("blah {") shouldBe None
}


it should "retrieve correctly hovered word in simple case" in {
val simpleText = "Hello SDLB!"
HoconParser.retrieveWordAtPosition(simpleText, 1, 0) shouldBe "Hello"
HoconParser.retrieveWordAtPosition(simpleText, 1, 4) shouldBe "Hello"
HoconParser.retrieveWordAtPosition(simpleText, 1, 5) shouldBe "Hello"
HoconParser.retrieveWordAtPosition(simpleText, 1, 6) shouldBe "SDLB!"
HoconParser.retrieveWordAtPosition(simpleText, 1, 10) shouldBe "SDLB!"
HoconParser.retrieveWordAtPosition(simpleText, 1, 11) shouldBe "SDLB!"
}

it should "retrieve correctly hovered word in simple case with multiple spaces" in {
val simpleText = "Hello SDLB!"
HoconParser.retrieveWordAtPosition(simpleText, 1, 0) shouldBe "Hello"
HoconParser.retrieveWordAtPosition(simpleText, 1, 4) shouldBe "Hello"
HoconParser.retrieveWordAtPosition(simpleText, 1, 5) shouldBe "Hello"
HoconParser.retrieveWordAtPosition(simpleText, 1, 6) shouldBe ""
HoconParser.retrieveWordAtPosition(simpleText, 1, 7) shouldBe "SDLB!"
HoconParser.retrieveWordAtPosition(simpleText, 1, 11) shouldBe "SDLB!"
HoconParser.retrieveWordAtPosition(simpleText, 1, 12) shouldBe "SDLB!"
}

it should "retrieve correctly hovered word in text" in {
val fixture = loadFixture("fixture/hocon/with-comments-example.conf")
val text = fixture.text
HoconParser.retrieveWordAtPosition(text, 4, 0) shouldBe "global"
HoconParser.retrieveWordAtPosition(text, 4, 6) shouldBe "global"
HoconParser.retrieveWordAtPosition(text, 4, 7) shouldBe "{"
HoconParser.retrieveWordAtPosition(text, 6, 7) shouldBe "#\"spark.sql.shuffle.partitions\"" //TODO its a comment line, disable?
HoconParser.retrieveWordAtPosition(text, 7, 7) shouldBe "\"spark.sql.shuffle.partitions\"" //TODO its a string value, disable?

}


private def validateText(fixture: Fixture, column: Int, caretDataList: List[CaretData], positionMap: Option[List[(Int, Int)]]=None): Unit =
Expand Down
25 changes: 25 additions & 0 deletions src/test/scala/io/smartdatalake/hover/SDLBHoverEngineSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.smartdatalake.hover

import io.smartdatalake.UnitSpec
import io.smartdatalake.context.SDLBContext
import ujson.*

import scala.io.Source
import scala.util.Using

class SDLBHoverEngineSpec extends UnitSpec {

val hoveringEngine = new SDLBHoverEngineImpl

"SDLB Completion engine" should "retrieve all the properties of copyAction" in {
val context = SDLBContext.fromText(loadFile("fixture/hocon/with-multi-lines-flattened-example.conf"))
.withCaretPosition(6, 4)
val expected =
"""Configuration of a custom Spark-DataFrame transformation between many inputs and many outputs (n:m).
|Define a transform function which receives a map of input DataObjectIds with DataFrames and a map of options and has
|to return a map of output DataObjectIds with DataFrames, see also trait[[CustomDfsTransformer]] .""".stripMargin
//hoveringEngine.generateHoveringInformation(context).getContents.getRight.getValue shouldBe expected
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class SmartDataLakeLanguageServerSpec extends UnitSpec {
capabilities.get().getCapabilities.getCompletionProvider shouldNot be (null)
}

"SDL Language Server" should "have hovering as a capability" in {
val capabilities: CompletableFuture[InitializeResult] = sdlLanguageServer.initialize(null)
capabilities.get().getCapabilities.getHoverProvider.getLeft shouldBe true
}

it should "exit without errors if shutdown before" in {
val server = sdlLanguageServer
server.shutdown()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,40 @@ package io.smartdatalake.languageserver

import io.smartdatalake.UnitSpec
import io.smartdatalake.languageserver.SmartDataLakeTextDocumentService
import org.eclipse.lsp4j.{CompletionParams, DidOpenTextDocumentParams, Position, TextDocumentItem}
import org.eclipse.lsp4j.{CompletionParams, DidOpenTextDocumentParams, HoverParams, Position, TextDocumentItem}

class SmartDataLakeTextDocumentServiceSpec extends UnitSpec {

val textDocumentService = new SmartDataLakeTextDocumentService

def params: CompletionParams =
val p = new CompletionParams()
// Careful, Position of LSP4J is 0-based
p.setPosition(new Position(16, 0))
p

"SDL text document service" should "suggest at least one autocompletion item" in {
notifyOpenFile()
val completionResult = textDocumentService.completion(params)
assert(completionResult.get.isLeft)
assert(completionResult.get().getLeft.size() > 0)
}

it should "provides hovering information" in {
notifyOpenFile()
val params = new HoverParams()
// Careful, Position of LSP4J is 0-based
params.setPosition(new Position(5, 4))
val hoverInformation = textDocumentService.hover(params)
assert(!hoverInformation.get().getContents.getRight.getValue.isBlank)
}
private def notifyOpenFile(): Unit = {
val didOpenTextDocumentParams: DidOpenTextDocumentParams = new DidOpenTextDocumentParams()
val textDocumentItem: TextDocumentItem = new TextDocumentItem()
textDocumentItem.setText(loadFile("fixture/hocon/with-multi-lines-example.conf"))
didOpenTextDocumentParams.setTextDocument(textDocumentItem)
textDocumentService.didOpen(didOpenTextDocumentParams)
val completionResult = textDocumentService.completion(params)
assert(completionResult.get.isLeft)
assert(completionResult.get().getLeft.size() > 0)
}


}

0 comments on commit 4083f6f

Please sign in to comment.