diff --git a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts index 315356836b5..8139a8c4532 100644 --- a/plugins/amazonq/shared/jetbrains-community/build.gradle.kts +++ b/plugins/amazonq/shared/jetbrains-community/build.gradle.kts @@ -22,5 +22,10 @@ dependencies { implementation(libs.commons.collections) implementation(libs.nimbus.jose.jwt) + // FIX_WHEN_MIN_IS_242 + if (providers.gradleProperty("ideProfileName").get() == "2024.1") { + implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.24.0") + } + testFixturesApi(testFixtures(project(":plugin-core:jetbrains-community"))) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt new file mode 100644 index 00000000000..8932881568f --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClient.kt @@ -0,0 +1,18 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.services.LanguageClient +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata +import java.util.concurrent.CompletableFuture + +/** + * Requests sent by server to client + */ +@Suppress("unused") +interface AmazonQLanguageClient : LanguageClient { + @JsonRequest("aws/credentials/getConnectionMetadata") + fun getConnectionMetadata(): CompletableFuture +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt new file mode 100644 index 00000000000..ce8aa6368a4 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt @@ -0,0 +1,64 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import com.intellij.notification.NotificationType +import org.eclipse.lsp4j.ConfigurationParams +import org.eclipse.lsp4j.MessageActionItem +import org.eclipse.lsp4j.MessageParams +import org.eclipse.lsp4j.MessageType +import org.eclipse.lsp4j.PublishDiagnosticsParams +import org.eclipse.lsp4j.ShowMessageRequestParams +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData +import java.util.concurrent.CompletableFuture + +/** + * Concrete implementation of [AmazonQLanguageClient] to handle messages sent from server + */ +class AmazonQLanguageClientImpl : AmazonQLanguageClient { + override fun telemetryEvent(`object`: Any) { + println(`object`) + } + + override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams) { + println(diagnostics) + } + + override fun showMessage(messageParams: MessageParams) { + val type = when (messageParams.type) { + MessageType.Error -> NotificationType.ERROR + MessageType.Warning -> NotificationType.WARNING + MessageType.Info, MessageType.Log -> NotificationType.INFORMATION + } + println("$type: ${messageParams.message}") + } + + override fun showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture? { + println(requestParams) + + return CompletableFuture.completedFuture(null) + } + + override fun logMessage(message: MessageParams) { + showMessage(message) + } + + override fun getConnectionMetadata() = CompletableFuture.completedFuture( + ConnectionMetadata( + SsoProfileData("TODO") + ) + ) + + override fun configuration(params: ConfigurationParams): CompletableFuture> { + if (params.items.isEmpty()) { + return CompletableFuture.completedFuture(null) + } + + return CompletableFuture.completedFuture( + buildList { + } + ) + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt new file mode 100644 index 00000000000..6a40867a7e0 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageServer.kt @@ -0,0 +1,19 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage +import org.eclipse.lsp4j.jsonrpc.services.JsonRequest +import org.eclipse.lsp4j.services.LanguageServer +import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload +import java.util.concurrent.CompletableFuture + +/** + * Remote interface exposed by the Amazon Q language server + */ +@Suppress("unused") +interface AmazonQLanguageServer : LanguageServer { + @JsonRequest("aws/credentials/token/update") + fun updateTokenCredentials(payload: UpdateCredentialsPayload): CompletableFuture +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt new file mode 100644 index 00000000000..16b9d8810b6 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -0,0 +1,160 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp + +import com.google.gson.ToNumberPolicy +import com.intellij.execution.configurations.GeneralCommandLine +import com.intellij.execution.impl.ExecutionManagerImpl +import com.intellij.execution.process.KillableColoredProcessHandler +import com.intellij.execution.process.KillableProcessHandler +import com.intellij.execution.process.ProcessEvent +import com.intellij.execution.process.ProcessListener +import com.intellij.execution.process.ProcessOutputType +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.util.io.await +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.eclipse.lsp4j.InitializeParams +import org.eclipse.lsp4j.InitializedParams +import org.eclipse.lsp4j.jsonrpc.Launcher +import org.eclipse.lsp4j.launch.LSPLauncher +import org.slf4j.event.Level +import software.aws.toolkits.core.utils.getLogger +import software.aws.toolkits.core.utils.warn +import software.aws.toolkits.jetbrains.isDeveloperMode +import java.io.IOException +import java.io.OutputStreamWriter +import java.io.PipedInputStream +import java.io.PipedOutputStream +import java.io.PrintWriter +import java.io.StringWriter +import java.nio.charset.StandardCharsets +import java.util.concurrent.Future + +// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java +// JB impl and redhat both use a wrapper to handle input buffering issue +internal class LSPProcessListener : ProcessListener { + private val outputStream = PipedOutputStream() + private val outputStreamWriter = OutputStreamWriter(outputStream, StandardCharsets.UTF_8) + val inputStream = PipedInputStream(outputStream) + + override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) { + if (ProcessOutputType.isStdout(outputType)) { + try { + this.outputStreamWriter.write(event.text) + this.outputStreamWriter.flush() + } catch (_: IOException) { + ExecutionManagerImpl.stopProcess(event.processHandler) + } + } else if (ProcessOutputType.isStderr(outputType)) { + LOG.warn { "LSP process stderr: ${event.text}" } + } + } + + override fun processTerminated(event: ProcessEvent) { + try { + this.outputStreamWriter.close() + this.outputStream.close() + } catch (_: IOException) { + } + } + + companion object { + private val LOG = getLogger() + } +} + +@Service(Service.Level.PROJECT) +class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disposable { + private val launcher: Launcher + + private val languageServer: AmazonQLanguageServer + get() = launcher.remoteProxy + + @Suppress("ForbiddenVoid") + private val launcherFuture: Future + private val launcherHandler: KillableProcessHandler + + init { + val cmd = GeneralCommandLine("amazon-q-lsp") + + launcherHandler = KillableColoredProcessHandler.Silent(cmd) + val inputWrapper = LSPProcessListener() + launcherHandler.addProcessListener(inputWrapper) + launcherHandler.startNotify() + + launcher = LSPLauncher.Builder() + .setLocalService(AmazonQLanguageClientImpl()) + .setRemoteInterface(AmazonQLanguageServer::class.java) + .configureGson { + // TODO: maybe need adapter for initialize: + // https://github.com/aws/amazon-q-eclipse/blob/b9d5bdcd5c38e1dd8ad371d37ab93a16113d7d4b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java + + // otherwise Gson treats all numbers as double which causes deser issues + it.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE) + }.traceMessages( + PrintWriter( + object : StringWriter() { + private val traceLogger = LOG.atLevel(if (isDeveloperMode()) Level.INFO else Level.DEBUG) + + override fun flush() { + traceLogger.log { buffer.toString() } + buffer.setLength(0) + } + } + ) + ) + .setInput(inputWrapper.inputStream) + .setOutput(launcherHandler.process.outputStream) + .create() + + launcherFuture = launcher.startListening() + + cs.launch { + val initializeResult = languageServer.initialize( + InitializeParams().apply { + // does this work on windows + processId = ProcessHandle.current().pid().toInt() + // capabilities + // client info + // trace? + // workspace folders? + // anything else we need? + } + // probably need a timeout + ).await() + + // then if this succeeds then we can allow the client to send requests + if (initializeResult == null) { + LOG.warn { "LSP initialization failed" } + launcherHandler.destroyProcess() + } + languageServer.initialized(InitializedParams()) + } + } + + override fun dispose() { + if (!launcherFuture.isDone) { + try { + languageServer.apply { + shutdown().thenRun { exit() } + } + } catch (e: Exception) { + LOG.warn(e) { "LSP shutdown failed" } + launcherHandler.destroyProcess() + } + } else if (!launcherHandler.isProcessTerminated) { + launcherHandler.destroyProcess() + } + } + + companion object { + private val LOG = getLogger() + fun getInstance(project: Project) = project.service() + } +} diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt new file mode 100644 index 00000000000..c6216b97cff --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/ConnectionMetadata.kt @@ -0,0 +1,12 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials + +data class ConnectionMetadata( + val sso: SsoProfileData, +) + +data class SsoProfileData( + val startUrl: String, +) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt new file mode 100644 index 00000000000..2b5a28b6cb4 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt @@ -0,0 +1,17 @@ +// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials + +data class UpdateCredentialsPayload( + val data: String, + val encrypted: String, +) + +data class UpdateCredentialsPayloadData( + val data: BearerCredentials, +) + +data class BearerCredentials( + val token: String, +)