Skip to content

Commit 1c435be

Browse files
rlileigaol
authored andcommitted
feat(q): stub out Q LSP logic (aws#5352)
This is the initial iteration of the bare minimum of process management to interact with Amazon Q logic vended by Flare. It is a direct port of the exploratory work done in https://github.com/rli/lsp-exp/tree/master/src/main/kotlin/org/example/lsp4j Missing are tests and any sort of edge case handling
1 parent d7b828d commit 1c435be

File tree

7 files changed

+295
-0
lines changed

7 files changed

+295
-0
lines changed

plugins/amazonq/shared/jetbrains-community/build.gradle.kts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,10 @@ dependencies {
2222
implementation(libs.commons.collections)
2323
implementation(libs.nimbus.jose.jwt)
2424

25+
// FIX_WHEN_MIN_IS_242
26+
if (providers.gradleProperty("ideProfileName").get() == "2024.1") {
27+
implementation("org.eclipse.lsp4j:org.eclipse.lsp4j:0.24.0")
28+
}
29+
2530
testFixturesApi(testFixtures(project(":plugin-core:jetbrains-community")))
2631
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp
5+
6+
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
7+
import org.eclipse.lsp4j.services.LanguageClient
8+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
9+
import java.util.concurrent.CompletableFuture
10+
11+
/**
12+
* Requests sent by server to client
13+
*/
14+
@Suppress("unused")
15+
interface AmazonQLanguageClient : LanguageClient {
16+
@JsonRequest("aws/credentials/getConnectionMetadata")
17+
fun getConnectionMetadata(): CompletableFuture<ConnectionMetadata>
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp
5+
6+
import com.intellij.notification.NotificationType
7+
import org.eclipse.lsp4j.ConfigurationParams
8+
import org.eclipse.lsp4j.MessageActionItem
9+
import org.eclipse.lsp4j.MessageParams
10+
import org.eclipse.lsp4j.MessageType
11+
import org.eclipse.lsp4j.PublishDiagnosticsParams
12+
import org.eclipse.lsp4j.ShowMessageRequestParams
13+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata
14+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.SsoProfileData
15+
import java.util.concurrent.CompletableFuture
16+
17+
/**
18+
* Concrete implementation of [AmazonQLanguageClient] to handle messages sent from server
19+
*/
20+
class AmazonQLanguageClientImpl : AmazonQLanguageClient {
21+
override fun telemetryEvent(`object`: Any) {
22+
println(`object`)
23+
}
24+
25+
override fun publishDiagnostics(diagnostics: PublishDiagnosticsParams) {
26+
println(diagnostics)
27+
}
28+
29+
override fun showMessage(messageParams: MessageParams) {
30+
val type = when (messageParams.type) {
31+
MessageType.Error -> NotificationType.ERROR
32+
MessageType.Warning -> NotificationType.WARNING
33+
MessageType.Info, MessageType.Log -> NotificationType.INFORMATION
34+
}
35+
println("$type: ${messageParams.message}")
36+
}
37+
38+
override fun showMessageRequest(requestParams: ShowMessageRequestParams): CompletableFuture<MessageActionItem?>? {
39+
println(requestParams)
40+
41+
return CompletableFuture.completedFuture(null)
42+
}
43+
44+
override fun logMessage(message: MessageParams) {
45+
showMessage(message)
46+
}
47+
48+
override fun getConnectionMetadata() = CompletableFuture.completedFuture(
49+
ConnectionMetadata(
50+
SsoProfileData("TODO")
51+
)
52+
)
53+
54+
override fun configuration(params: ConfigurationParams): CompletableFuture<List<Any>> {
55+
if (params.items.isEmpty()) {
56+
return CompletableFuture.completedFuture(null)
57+
}
58+
59+
return CompletableFuture.completedFuture(
60+
buildList {
61+
}
62+
)
63+
}
64+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp
5+
6+
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
7+
import org.eclipse.lsp4j.jsonrpc.services.JsonRequest
8+
import org.eclipse.lsp4j.services.LanguageServer
9+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.UpdateCredentialsPayload
10+
import java.util.concurrent.CompletableFuture
11+
12+
/**
13+
* Remote interface exposed by the Amazon Q language server
14+
*/
15+
@Suppress("unused")
16+
interface AmazonQLanguageServer : LanguageServer {
17+
@JsonRequest("aws/credentials/token/update")
18+
fun updateTokenCredentials(payload: UpdateCredentialsPayload): CompletableFuture<ResponseMessage>
19+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp
5+
6+
import com.google.gson.ToNumberPolicy
7+
import com.intellij.execution.configurations.GeneralCommandLine
8+
import com.intellij.execution.impl.ExecutionManagerImpl
9+
import com.intellij.execution.process.KillableColoredProcessHandler
10+
import com.intellij.execution.process.KillableProcessHandler
11+
import com.intellij.execution.process.ProcessEvent
12+
import com.intellij.execution.process.ProcessListener
13+
import com.intellij.execution.process.ProcessOutputType
14+
import com.intellij.openapi.Disposable
15+
import com.intellij.openapi.components.Service
16+
import com.intellij.openapi.components.service
17+
import com.intellij.openapi.project.Project
18+
import com.intellij.openapi.util.Key
19+
import com.intellij.util.io.await
20+
import kotlinx.coroutines.CoroutineScope
21+
import kotlinx.coroutines.launch
22+
import org.eclipse.lsp4j.InitializeParams
23+
import org.eclipse.lsp4j.InitializedParams
24+
import org.eclipse.lsp4j.jsonrpc.Launcher
25+
import org.eclipse.lsp4j.launch.LSPLauncher
26+
import org.slf4j.event.Level
27+
import software.aws.toolkits.core.utils.getLogger
28+
import software.aws.toolkits.core.utils.warn
29+
import software.aws.toolkits.jetbrains.isDeveloperMode
30+
import java.io.IOException
31+
import java.io.OutputStreamWriter
32+
import java.io.PipedInputStream
33+
import java.io.PipedOutputStream
34+
import java.io.PrintWriter
35+
import java.io.StringWriter
36+
import java.nio.charset.StandardCharsets
37+
import java.util.concurrent.Future
38+
39+
// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
40+
// JB impl and redhat both use a wrapper to handle input buffering issue
41+
internal class LSPProcessListener : ProcessListener {
42+
private val outputStream = PipedOutputStream()
43+
private val outputStreamWriter = OutputStreamWriter(outputStream, StandardCharsets.UTF_8)
44+
val inputStream = PipedInputStream(outputStream)
45+
46+
override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
47+
if (ProcessOutputType.isStdout(outputType)) {
48+
try {
49+
this.outputStreamWriter.write(event.text)
50+
this.outputStreamWriter.flush()
51+
} catch (_: IOException) {
52+
ExecutionManagerImpl.stopProcess(event.processHandler)
53+
}
54+
} else if (ProcessOutputType.isStderr(outputType)) {
55+
LOG.warn { "LSP process stderr: ${event.text}" }
56+
}
57+
}
58+
59+
override fun processTerminated(event: ProcessEvent) {
60+
try {
61+
this.outputStreamWriter.close()
62+
this.outputStream.close()
63+
} catch (_: IOException) {
64+
}
65+
}
66+
67+
companion object {
68+
private val LOG = getLogger<LSPProcessListener>()
69+
}
70+
}
71+
72+
@Service(Service.Level.PROJECT)
73+
class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disposable {
74+
private val launcher: Launcher<AmazonQLanguageServer>
75+
76+
private val languageServer: AmazonQLanguageServer
77+
get() = launcher.remoteProxy
78+
79+
@Suppress("ForbiddenVoid")
80+
private val launcherFuture: Future<Void>
81+
private val launcherHandler: KillableProcessHandler
82+
83+
init {
84+
val cmd = GeneralCommandLine("amazon-q-lsp")
85+
86+
launcherHandler = KillableColoredProcessHandler.Silent(cmd)
87+
val inputWrapper = LSPProcessListener()
88+
launcherHandler.addProcessListener(inputWrapper)
89+
launcherHandler.startNotify()
90+
91+
launcher = LSPLauncher.Builder<AmazonQLanguageServer>()
92+
.setLocalService(AmazonQLanguageClientImpl())
93+
.setRemoteInterface(AmazonQLanguageServer::class.java)
94+
.configureGson {
95+
// TODO: maybe need adapter for initialize:
96+
// https://github.com/aws/amazon-q-eclipse/blob/b9d5bdcd5c38e1dd8ad371d37ab93a16113d7d4b/plugin/src/software/aws/toolkits/eclipse/amazonq/lsp/QLspTypeAdapterFactory.java
97+
98+
// otherwise Gson treats all numbers as double which causes deser issues
99+
it.setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
100+
}.traceMessages(
101+
PrintWriter(
102+
object : StringWriter() {
103+
private val traceLogger = LOG.atLevel(if (isDeveloperMode()) Level.INFO else Level.DEBUG)
104+
105+
override fun flush() {
106+
traceLogger.log { buffer.toString() }
107+
buffer.setLength(0)
108+
}
109+
}
110+
)
111+
)
112+
.setInput(inputWrapper.inputStream)
113+
.setOutput(launcherHandler.process.outputStream)
114+
.create()
115+
116+
launcherFuture = launcher.startListening()
117+
118+
cs.launch {
119+
val initializeResult = languageServer.initialize(
120+
InitializeParams().apply {
121+
// does this work on windows
122+
processId = ProcessHandle.current().pid().toInt()
123+
// capabilities
124+
// client info
125+
// trace?
126+
// workspace folders?
127+
// anything else we need?
128+
}
129+
// probably need a timeout
130+
).await()
131+
132+
// then if this succeeds then we can allow the client to send requests
133+
if (initializeResult == null) {
134+
LOG.warn { "LSP initialization failed" }
135+
launcherHandler.destroyProcess()
136+
}
137+
languageServer.initialized(InitializedParams())
138+
}
139+
}
140+
141+
override fun dispose() {
142+
if (!launcherFuture.isDone) {
143+
try {
144+
languageServer.apply {
145+
shutdown().thenRun { exit() }
146+
}
147+
} catch (e: Exception) {
148+
LOG.warn(e) { "LSP shutdown failed" }
149+
launcherHandler.destroyProcess()
150+
}
151+
} else if (!launcherHandler.isProcessTerminated) {
152+
launcherHandler.destroyProcess()
153+
}
154+
}
155+
156+
companion object {
157+
private val LOG = getLogger<AmazonQLspService>()
158+
fun getInstance(project: Project) = project.service<AmazonQLspService>()
159+
}
160+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials
5+
6+
data class ConnectionMetadata(
7+
val sso: SsoProfileData,
8+
)
9+
10+
data class SsoProfileData(
11+
val startUrl: String,
12+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials
5+
6+
data class UpdateCredentialsPayload(
7+
val data: String,
8+
val encrypted: String,
9+
)
10+
11+
data class UpdateCredentialsPayloadData(
12+
val data: BearerCredentials,
13+
)
14+
15+
data class BearerCredentials(
16+
val token: String,
17+
)

0 commit comments

Comments
 (0)