diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml index f8c0428..f1fa0a4 100644 --- a/src/main/resources/logback.xml +++ b/src/main/resources/logback.xml @@ -2,9 +2,9 @@ - logs/myapp.log + logs/sdl-lsp.log - logs/myapp.%i.log + logs/sdl-lsp.%i.log 1 10 @@ -21,4 +21,4 @@ - + \ No newline at end of file diff --git a/src/main/scala/io/smartdatalake/Main.scala b/src/main/scala/io/smartdatalake/Main.scala index a037a33..9d27901 100644 --- a/src/main/scala/io/smartdatalake/Main.scala +++ b/src/main/scala/io/smartdatalake/Main.scala @@ -1,14 +1,20 @@ package io.smartdatalake -import io.smartdatalake.logging.LoggingManager +import ch.qos.logback.classic.Level +import io.smartdatalake.logging.{LoggerOutputStream, LoggingManager} import io.smartdatalake.modules.AppModule +import jdk.jshell.spi.ExecutionControlProvider import org.eclipse.lsp4j.jsonrpc.Launcher import org.eclipse.lsp4j.launch.LSPLauncher import org.eclipse.lsp4j.services.{LanguageClient, LanguageClientAware, LanguageServer} -import java.io.{InputStream, OutputStream, PrintStream} +import java.io.{InputStream, OutputStream, PrintStream, PrintWriter} import org.slf4j.LoggerFactory +import java.util.concurrent.Executors +import scala.concurrent.ExecutionContext +import scala.util.control.NonFatal + /** * @author scalathe */ @@ -29,15 +35,34 @@ object Main extends AppModule { } private def startServer(in: InputStream, out: PrintStream) = { + val logger = LoggerFactory.getLogger(getClass) val helloLanguageServer: LanguageServer & LanguageClientAware = languageServer - val launcher: Launcher[LanguageClient] = LSPLauncher.createServerLauncher(helloLanguageServer, in, out) - val client: LanguageClient = launcher.getRemoteProxy - helloLanguageServer.connect(client) - // Use the configured logger - val logger = LoggerFactory.getLogger(getClass) - logger.info("Server starts listening...") - launcher.startListening().get() + try + val launcher: Launcher[LanguageClient] = Launcher.Builder[LanguageClient]() + .traceMessages(PrintWriter(LoggingManager.createPrintStreamWithLoggerName("jsonRpcLogger", level = Level.TRACE))) + .setExecutorService(executorService) + .setInput(in) + .setOutput(out) + .setRemoteInterface(classOf[LanguageClient]) + .setLocalService(helloLanguageServer) + .create() + + val client: LanguageClient = launcher.getRemoteProxy + helloLanguageServer.connect(client) + // Use the configured logger + logger.info("Server starts listening...") + launcher.startListening().get() + catch + case NonFatal(ex) => + ex.printStackTrace(out) + logger.error(ex.toString) + + finally + // Might want to also give capabilities to let the server shutdown itself more properly + executionContext.shutdownNow() + executorService.shutdownNow() + sys.exit(0) } } diff --git a/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala b/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala index 741fd4b..a85734e 100644 --- a/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala +++ b/src/main/scala/io/smartdatalake/completion/SDLBCompletionEngineImpl.scala @@ -8,9 +8,7 @@ import org.eclipse.lsp4j.{CompletionItem, CompletionItemKind} import scala.util.{Failure, Success, Try} class SDLBCompletionEngineImpl(private val schemaReader: SchemaReader) extends SDLBCompletionEngine { - - //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 generateCompletionItems(context: SDLBContext): List[CompletionItem] = context.parentPath match case path if path.startsWith("actions") && path.count(_ == '.') == 1 => generatePropertiesOfAction(context) case "actions" => generateTemplatesForAction() diff --git a/src/main/scala/io/smartdatalake/context/TextContext.scala b/src/main/scala/io/smartdatalake/context/TextContext.scala index 0ae3d0f..44abc86 100644 --- a/src/main/scala/io/smartdatalake/context/TextContext.scala +++ b/src/main/scala/io/smartdatalake/context/TextContext.scala @@ -12,7 +12,7 @@ case class TextContext private (originalText: String, configText: String, config case _ => updateContext(newText) private def updateContext(newText: String) = - val newConfigText = MultiLineTransformer.flattenMultiLines(newText) // For now. We'll see how to optimize incr. parsing and how to handle multiple files later + val newConfigText = MultiLineTransformer.flattenMultiLines(newText) val newConfig = HoconParser.parse(newConfigText).getOrElse(HoconParser.EMPTY_CONFIG) if newConfig == HoconParser.EMPTY_CONFIG then this else TextContext(newText, newConfigText, newConfig) diff --git a/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala b/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala new file mode 100644 index 0000000..d395e5a --- /dev/null +++ b/src/main/scala/io/smartdatalake/conversions/ScalaJavaConverter.scala @@ -0,0 +1,21 @@ +package io.smartdatalake.conversions + +import java.util.concurrent.CompletableFuture +import org.eclipse.lsp4j.jsonrpc.messages +import scala.concurrent.Future +import scala.jdk.FutureConverters.* +import scala.jdk.CollectionConverters.* + +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 [L, R] (either: Either[L, R]) def toJava: messages.Either[L, R] = either match + case Left(leftValue) => messages.Either.forLeft(leftValue) + case Right(rightValue) => messages.Either.forRight(rightValue) + +} + +object ScalaJavaConverterAPI extends ScalaJavaConverter diff --git a/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeLanguageServer.scala b/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeLanguageServer.scala index 62826f0..a1240af 100644 --- a/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeLanguageServer.scala +++ b/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeLanguageServer.scala @@ -1,25 +1,26 @@ package io.smartdatalake.languageserver import io.smartdatalake.languageserver.{SmartDataLakeTextDocumentService, SmartDataLakeWorkspaceService} +import io.smartdatalake.conversions.ScalaJavaConverterAPI.* import org.eclipse.lsp4j.services.* import org.eclipse.lsp4j.* import java.util.concurrent.CompletableFuture +import scala.concurrent.{ExecutionContext, Future} -class SmartDataLakeLanguageServer(private val textDocumentService: TextDocumentService, private val workspaceService: WorkspaceService) extends LanguageServer with LanguageClientAware { - +class SmartDataLakeLanguageServer(private val textDocumentService: TextDocumentService, private val workspaceService: WorkspaceService)(using ExecutionContext) extends LanguageServer with LanguageClientAware { + private var client: Option[LanguageClient] = None private var errorCode = 1 override def initialize(initializeParams: InitializeParams): CompletableFuture[InitializeResult] = { - val initializeResult = new InitializeResult(new ServerCapabilities) + val initializeResult = InitializeResult(ServerCapabilities()) initializeResult.getCapabilities.setTextDocumentSync(TextDocumentSyncKind.Full) - val completionOptions = new CompletionOptions + val completionOptions = CompletionOptions() initializeResult.getCapabilities.setCompletionProvider(completionOptions) initializeResult.getCapabilities.setHoverProvider(true) - - CompletableFuture.supplyAsync(() => initializeResult) + Future(initializeResult).toJava } override def shutdown(): CompletableFuture[AnyRef] = { diff --git a/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeTextDocumentService.scala b/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeTextDocumentService.scala index d16405f..4355d8e 100644 --- a/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeTextDocumentService.scala +++ b/src/main/scala/io/smartdatalake/languageserver/SmartDataLakeTextDocumentService.scala @@ -4,29 +4,29 @@ import io.smartdatalake.completion.{SDLBCompletionEngine, SDLBCompletionEngineIm import io.smartdatalake.context.SDLBContext import io.smartdatalake.hover.{SDLBHoverEngine, SDLBHoverEngineImpl} import io.smartdatalake.schema.SchemaReader +import io.smartdatalake.conversions.ScalaJavaConverterAPI.* 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, MarkupContent, MarkupKind, Position, Range, ReferenceParams, RenameParams, SignatureHelp, SignatureHelpParams, SymbolInformation, TextDocumentPositionParams, TextEdit, WorkspaceEdit} import java.util import java.util.concurrent.CompletableFuture +import scala.concurrent.{ExecutionContext, Future} import scala.io.Source import scala.util.Using -class SmartDataLakeTextDocumentService(private val completionEngine: SDLBCompletionEngine, private val hoverEngine: SDLBHoverEngine) extends TextDocumentService { +class SmartDataLakeTextDocumentService(private val completionEngine: SDLBCompletionEngine, private val hoverEngine: SDLBHoverEngine)(using ExecutionContext) extends TextDocumentService { private var context: SDLBContext = SDLBContext.EMPTY_CONTEXT override def completion(params: CompletionParams): CompletableFuture[messages.Either[util.List[CompletionItem], CompletionList]] = { - CompletableFuture.supplyAsync(() => { - context = context.withCaretPosition(params.getPosition.getLine+1, params.getPosition.getCharacter) - val completionItems = new util.ArrayList[CompletionItem]() - val suggestions: List[CompletionItem] = completionEngine.generateCompletionItems(context) - suggestions.foreach(e => completionItems.add(e)) + Future { + val caretContext = context.withCaretPosition(params.getPosition.getLine+1, params.getPosition.getCharacter) + val completionItems: util.List[CompletionItem] = completionEngine.generateCompletionItems(caretContext).toJava + Left(completionItems).toJava + }.toJava - messages.Either.forLeft(completionItems).asInstanceOf[messages.Either[util.List[CompletionItem], CompletionList]] - }) } override def didOpen(didOpenTextDocumentParams: DidOpenTextDocumentParams): Unit = @@ -50,10 +50,10 @@ class SmartDataLakeTextDocumentService(private val completionEngine: SDLBComplet override def resolveCompletionItem(completionItem: CompletionItem): CompletableFuture[CompletionItem] = ??? override def hover(params: HoverParams): CompletableFuture[Hover] = { - CompletableFuture.supplyAsync(() => { + Future { val hoverContext = context.withCaretPosition(params.getPosition.getLine + 1, params.getPosition.getCharacter) hoverEngine.generateHoveringInformation(hoverContext) - }) + }.toJava } override def signatureHelp(params: SignatureHelpParams): CompletableFuture[SignatureHelp] = super.signatureHelp(params) diff --git a/src/main/scala/io/smartdatalake/logging/LoggerOutputStream.scala b/src/main/scala/io/smartdatalake/logging/LoggerOutputStream.scala index bd15bf8..88826ab 100644 --- a/src/main/scala/io/smartdatalake/logging/LoggerOutputStream.scala +++ b/src/main/scala/io/smartdatalake/logging/LoggerOutputStream.scala @@ -1,14 +1,15 @@ package io.smartdatalake.logging +import ch.qos.logback.classic.Level import org.slf4j.Logger import java.io.OutputStream -private[logging] class LoggerOutputStream(logger: Logger) extends OutputStream { +private[logging] class LoggerOutputStream(write: String => Unit) extends OutputStream { private val builder = new StringBuilder override def write(b: Int): Unit = { if (b == '\n') { - logger.info(builder.toString) + write(builder.toString) builder.clear() } else { builder.append(b.toChar) diff --git a/src/main/scala/io/smartdatalake/logging/LoggingManager.scala b/src/main/scala/io/smartdatalake/logging/LoggingManager.scala index ef0ab69..d74f8af 100644 --- a/src/main/scala/io/smartdatalake/logging/LoggingManager.scala +++ b/src/main/scala/io/smartdatalake/logging/LoggingManager.scala @@ -1,5 +1,6 @@ package io.smartdatalake.logging +import ch.qos.logback.classic.Level import org.slf4j.LoggerFactory import java.io.PrintStream @@ -7,9 +8,18 @@ import java.io.PrintStream object LoggingManager { def redirectStandardOutputToLoggerOutput(): Unit = - val loggerRedirectedOutput = LoggerFactory.getLogger("redirectedOutput") - val redirectedPrintStream = new PrintStream(new LoggerOutputStream(loggerRedirectedOutput)) + val redirectedPrintStream = createPrintStreamWithLoggerName("redirectedOutput") System.setOut(redirectedPrintStream) println("Using new default output stream") + def createPrintStreamWithLoggerName(loggerName: String, level: Level = Level.INFO): PrintStream = + val logger = LoggerFactory.getLogger(loggerName) + val printMethod: String => Unit = level match + case Level.TRACE => logger.trace + case Level.DEBUG => logger.debug + case Level.INFO => logger.info + case Level.WARN => logger.warn + case Level.ERROR => logger.error + PrintStream(LoggerOutputStream(printMethod)) + } diff --git a/src/main/scala/io/smartdatalake/modules/AppModule.scala b/src/main/scala/io/smartdatalake/modules/AppModule.scala index d4949e3..31779b3 100644 --- a/src/main/scala/io/smartdatalake/modules/AppModule.scala +++ b/src/main/scala/io/smartdatalake/modules/AppModule.scala @@ -6,13 +6,18 @@ import io.smartdatalake.languageserver.{SmartDataLakeLanguageServer, SmartDataLa import io.smartdatalake.schema.{SchemaReader, SchemaReaderImpl} import org.eclipse.lsp4j.services.{LanguageClientAware, LanguageServer, TextDocumentService, WorkspaceService} +import java.util.concurrent.{ExecutorService, Executors} +import scala.concurrent.{ExecutionContext, ExecutionContextExecutorService} + trait AppModule { lazy val schemaReader: SchemaReader = new SchemaReaderImpl("sdl-schema/sdl-schema-2.5.0.json") lazy val completionEngine: SDLBCompletionEngine = new SDLBCompletionEngineImpl(schemaReader) lazy val hoverEngine: SDLBHoverEngine = new SDLBHoverEngineImpl(schemaReader) - lazy val textDocumentService: TextDocumentService = new SmartDataLakeTextDocumentService(completionEngine, hoverEngine) + lazy val executorService: ExecutorService = Executors.newCachedThreadPool() + lazy val executionContext: ExecutionContext & ExecutorService = ExecutionContext.fromExecutorService(executorService) + lazy val textDocumentService: TextDocumentService = new SmartDataLakeTextDocumentService(completionEngine, hoverEngine)(using executionContext) lazy val workspaceService: WorkspaceService = new SmartDataLakeWorkspaceService - lazy val languageServer: LanguageServer & LanguageClientAware = new SmartDataLakeLanguageServer(textDocumentService, workspaceService) + lazy val languageServer: LanguageServer & LanguageClientAware = new SmartDataLakeLanguageServer(textDocumentService, workspaceService)(using executionContext) } diff --git a/src/test/scala/io/smartdatalake/modules/TestModule.scala b/src/test/scala/io/smartdatalake/modules/TestModule.scala index 5868fef..50579ee 100644 --- a/src/test/scala/io/smartdatalake/modules/TestModule.scala +++ b/src/test/scala/io/smartdatalake/modules/TestModule.scala @@ -6,6 +6,6 @@ import io.smartdatalake.schema.{SchemaReader, SchemaReaderImpl} trait TestModule extends AppModule { override lazy val schemaReader: SchemaReader = new SchemaReaderImpl("fixture/sdl-schema/sdl-schema-2.5.0.json") override lazy val completionEngine: SDLBCompletionEngineImpl = new SDLBCompletionEngineImpl(schemaReader) - override lazy val languageServer: SmartDataLakeLanguageServer = new SmartDataLakeLanguageServer(textDocumentService, workspaceService) + override lazy val languageServer: SmartDataLakeLanguageServer = new SmartDataLakeLanguageServer(textDocumentService, workspaceService)(using executionContext) }