33
44package software.aws.toolkits.jetbrains.services.amazonq.lsp
55
6+ import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
67import com.google.gson.ToNumberPolicy
78import com.intellij.execution.configurations.GeneralCommandLine
89import com.intellij.execution.impl.ExecutionManagerImpl
@@ -34,19 +35,40 @@ import org.eclipse.lsp4j.ClientInfo
3435import org.eclipse.lsp4j.FileOperationsWorkspaceCapabilities
3536import org.eclipse.lsp4j.InitializeParams
3637import org.eclipse.lsp4j.InitializedParams
38+ import org.eclipse.lsp4j.MessageActionItem
39+ import org.eclipse.lsp4j.MessageParams
40+ import org.eclipse.lsp4j.PublishDiagnosticsParams
41+ import org.eclipse.lsp4j.ShowMessageRequestParams
3742import org.eclipse.lsp4j.SynchronizationCapabilities
3843import org.eclipse.lsp4j.TextDocumentClientCapabilities
3944import org.eclipse.lsp4j.WorkspaceClientCapabilities
4045import org.eclipse.lsp4j.WorkspaceFolder
4146import org.eclipse.lsp4j.jsonrpc.Launcher
47+ import org.eclipse.lsp4j.jsonrpc.MessageConsumer
48+ import org.eclipse.lsp4j.jsonrpc.messages.RequestMessage
49+ import org.eclipse.lsp4j.jsonrpc.services.JsonNotification
50+ import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
4251import org.eclipse.lsp4j.launch.LSPLauncher
52+ import org.eclipse.lsp4j.services.LanguageClient
53+ import org.eclipse.lsp4j.services.LanguageServer
4354import org.slf4j.event.Level
4455import software.aws.toolkits.core.utils.getLogger
4556import software.aws.toolkits.core.utils.info
4657import software.aws.toolkits.core.utils.warn
4758import software.aws.toolkits.jetbrains.isDeveloperMode
4859import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
4960import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtendedClientMetadata
61+ import software.aws.toolkits.jetbrains.services.amazonq.project.EncoderServer
62+ import software.aws.toolkits.jetbrains.services.amazonq.project.IndexRequest
63+ import software.aws.toolkits.jetbrains.services.amazonq.project.IndexUpdateMode
64+ import software.aws.toolkits.jetbrains.services.amazonq.project.InlineBm25Chunk
65+ import software.aws.toolkits.jetbrains.services.amazonq.project.LspMessage
66+ import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider
67+ import software.aws.toolkits.jetbrains.services.amazonq.project.ProjectContextProvider.Usage
68+ import software.aws.toolkits.jetbrains.services.amazonq.project.QueryChatRequest
69+ import software.aws.toolkits.jetbrains.services.amazonq.project.QueryInlineCompletionRequest
70+ import software.aws.toolkits.jetbrains.services.amazonq.project.RelevantDocument
71+ import software.aws.toolkits.jetbrains.services.amazonq.project.UpdateIndexRequest
5072import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
5173import java.io.IOException
5274import java.io.OutputStreamWriter
@@ -56,6 +78,7 @@ import java.io.PrintWriter
5678import java.io.StringWriter
5779import java.net.URI
5880import java.nio.charset.StandardCharsets
81+ import java.util.concurrent.CompletableFuture
5982import java.util.concurrent.Future
6083import kotlin.time.Duration.Companion.seconds
6184
@@ -324,3 +347,193 @@ private class AmazonQServerInstance(private val project: Project, private val cs
324347 private val LOG = getLogger<AmazonQServerInstance >()
325348 }
326349}
350+
351+
352+ class EncoderServer2 (private val encoderServer : EncoderServer , private val commandLine : GeneralCommandLine , private val project : Project , private val cs : CoroutineScope ) : Disposable {
353+ private val launcher: Launcher <EncoderServerLspInterface >
354+
355+ val languageServer: EncoderServerLspInterface
356+ get() = launcher.remoteProxy
357+
358+ @Suppress(" ForbiddenVoid" )
359+ private val launcherFuture: Future <Void >
360+ private val launcherHandler: KillableProcessHandler
361+ val initializer: Job
362+
363+ private fun createClientCapabilities (): ClientCapabilities =
364+ ClientCapabilities ().apply {
365+ textDocument = TextDocumentClientCapabilities ().apply {
366+ // For didSaveTextDocument, other textDocument/ messages always mandatory
367+ synchronization = SynchronizationCapabilities ().apply {
368+ didSave = true
369+ }
370+ }
371+
372+ workspace = WorkspaceClientCapabilities ().apply {
373+ applyEdit = false
374+
375+ // For workspace folder changes
376+ workspaceFolders = true
377+
378+ // For file operations (create, delete)
379+ fileOperations = FileOperationsWorkspaceCapabilities ().apply {
380+ didCreate = true
381+ didDelete = true
382+ }
383+ }
384+ }
385+
386+ // needs case handling when project's base path is null: default projects/unit tests
387+ private fun createWorkspaceFolders (): List <WorkspaceFolder > =
388+ project.basePath?.let { basePath ->
389+ listOf (
390+ WorkspaceFolder (
391+ URI (" file://$basePath " ).toString(),
392+ project.name
393+ )
394+ )
395+ }.orEmpty() // no folders to report or workspace not folder based
396+
397+ private fun createClientInfo (): ClientInfo {
398+ val metadata = ClientMetadata .getDefault()
399+ return ClientInfo ().apply {
400+ name = metadata.awsProduct.toString()
401+ version = metadata.awsVersion
402+ }
403+ }
404+
405+ private fun createInitializeParams (): InitializeParams =
406+ InitializeParams ().apply {
407+ processId = ProcessHandle .current().pid().toInt()
408+ capabilities = createClientCapabilities()
409+ clientInfo = createClientInfo()
410+ workspaceFolders = createWorkspaceFolders()
411+ initializationOptions = mapOf (
412+ " extensionPath" to encoderServer.cachePath.toAbsolutePath().toString()
413+ )
414+ }
415+
416+ init {
417+ launcherHandler = KillableColoredProcessHandler .Silent (commandLine)
418+ val inputWrapper = LSPProcessListener ()
419+ launcherHandler.addProcessListener(inputWrapper)
420+ launcherHandler.startNotify()
421+
422+ launcher = LSPLauncher .Builder <EncoderServerLspInterface >()
423+ .setLocalService(object : LanguageClient {
424+ override fun telemetryEvent (p0 : Any? ) {
425+ println (p0)
426+ }
427+
428+ override fun publishDiagnostics (p0 : PublishDiagnosticsParams ? ) {
429+ println (p0)
430+ }
431+
432+ override fun showMessage (p0 : MessageParams ? ) {
433+ println (p0)
434+ }
435+
436+ override fun showMessageRequest (p0 : ShowMessageRequestParams ? ): CompletableFuture <MessageActionItem ?>? {
437+ println (p0)
438+
439+ return CompletableFuture .completedFuture(null )
440+ }
441+
442+ override fun logMessage (p0 : MessageParams ? ) {
443+ println (p0)
444+ }
445+
446+ })
447+ .setRemoteInterface(EncoderServerLspInterface ::class .java)
448+ .configureGson {
449+ // TODO: maybe need adapter for initialize:
450+ // https://github.com/aws/amazon-q-eclipse/blob/b9d5bdcd5c38e1dd8ad371d37ab93a16113d7d4b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java
451+
452+ // otherwise Gson treats all numbers as double which causes deser issues
453+ it.setObjectToNumberStrategy(ToNumberPolicy .LONG_OR_DOUBLE )
454+ }.traceMessages(
455+ PrintWriter (
456+ object : StringWriter () {
457+ private val traceLogger = LOG .atLevel(if (isDeveloperMode()) Level .INFO else Level .DEBUG )
458+
459+ override fun flush () {
460+ traceLogger.log { buffer.toString() }
461+ buffer.setLength(0 )
462+ }
463+ }
464+ )
465+ )
466+ .wrapMessages { consumer ->
467+ MessageConsumer { message ->
468+ if (message is RequestMessage && message.params is LspMessage ) {
469+ message.params = encoderServer.encrypt(jacksonObjectMapper().writeValueAsString(message))
470+ }
471+ consumer.consume(message)
472+ }
473+ }
474+ .setInput(inputWrapper.inputStream)
475+ .setOutput(launcherHandler.process.outputStream)
476+ .create()
477+
478+ launcherFuture = launcher.startListening()
479+
480+ initializer = cs.launch {
481+ // encryption info must be sent within 5s or Flare process will exit
482+ launcherHandler.process.outputStream.write(encoderServer.getEncryptionRequest().toByteArray())
483+
484+ val initializeResult = try {
485+ withTimeout(5 .seconds) {
486+ languageServer.initialize(createInitializeParams()).await()
487+ }
488+ } catch (_: TimeoutCancellationException ) {
489+ LOG .warn { " LSP initialization timed out" }
490+ null
491+ } catch (e: Exception ) {
492+ LOG .warn(e) { " LSP initialization failed" }
493+ null
494+ }
495+
496+ // then if this succeeds then we can allow the client to send requests
497+ if (initializeResult == null ) {
498+ launcherHandler.destroyProcess()
499+ }
500+ languageServer.initialized(InitializedParams ())
501+ }
502+ }
503+
504+ override fun dispose () {
505+ if (! launcherFuture.isDone) {
506+ try {
507+ languageServer.apply {
508+ shutdown().thenRun { exit() }
509+ }
510+ } catch (e: Exception ) {
511+ LOG .warn(e) { " LSP shutdown failed" }
512+ launcherHandler.destroyProcess()
513+ }
514+ } else if (! launcherHandler.isProcessTerminated) {
515+ launcherHandler.destroyProcess()
516+ }
517+ }
518+
519+ companion object {
520+ private val LOG = getLogger<AmazonQServerInstance >()
521+ }
522+ }
523+
524+ interface EncoderServerLspInterface : LanguageServer {
525+ @JsonRequest(" lsp/queryInlineProjectContext" )
526+ fun queryInline (request : QueryInlineCompletionRequest ): CompletableFuture <List <InlineBm25Chunk >>
527+
528+ @JsonRequest(" lsp/getUsage" )
529+ fun getUsageMetrics (): CompletableFuture <Usage >
530+
531+ @JsonRequest(" lsp/query" )
532+ fun queryChat (request : QueryChatRequest ): CompletableFuture <List <ProjectContextProvider .Chunk >>
533+
534+ @JsonNotification(" lsp/updateIndexV2" )
535+ fun updateIndex (request : UpdateIndexRequest ): CompletableFuture <Void >
536+
537+ @JsonNotification(" lsp/buildIndex" )
538+ fun buildIndex (request : IndexRequest ): CompletableFuture <List <InlineBm25Chunk >>
539+ }
0 commit comments