Skip to content

Commit 6e63a5c

Browse files
committed
wip: use json-rpc for local workspace context
1 parent a6e7685 commit 6e63a5c

File tree

4 files changed

+266
-112
lines changed

4 files changed

+266
-112
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonq.lsp
55

6+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
67
import com.google.gson.ToNumberPolicy
78
import com.intellij.execution.configurations.GeneralCommandLine
89
import com.intellij.execution.impl.ExecutionManagerImpl
@@ -34,19 +35,40 @@ import org.eclipse.lsp4j.ClientInfo
3435
import org.eclipse.lsp4j.FileOperationsWorkspaceCapabilities
3536
import org.eclipse.lsp4j.InitializeParams
3637
import 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
3742
import org.eclipse.lsp4j.SynchronizationCapabilities
3843
import org.eclipse.lsp4j.TextDocumentClientCapabilities
3944
import org.eclipse.lsp4j.WorkspaceClientCapabilities
4045
import org.eclipse.lsp4j.WorkspaceFolder
4146
import 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
4251
import org.eclipse.lsp4j.launch.LSPLauncher
52+
import org.eclipse.lsp4j.services.LanguageClient
53+
import org.eclipse.lsp4j.services.LanguageServer
4354
import org.slf4j.event.Level
4455
import software.aws.toolkits.core.utils.getLogger
4556
import software.aws.toolkits.core.utils.info
4657
import software.aws.toolkits.core.utils.warn
4758
import software.aws.toolkits.jetbrains.isDeveloperMode
4859
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
4960
import 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
5072
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
5173
import java.io.IOException
5274
import java.io.OutputStreamWriter
@@ -56,6 +78,7 @@ import java.io.PrintWriter
5678
import java.io.StringWriter
5779
import java.net.URI
5880
import java.nio.charset.StandardCharsets
81+
import java.util.concurrent.CompletableFuture
5982
import java.util.concurrent.Future
6083
import 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+
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/EncoderServer.kt

Lines changed: 19 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,14 @@ import com.nimbusds.jose.JWSHeader
1919
import com.nimbusds.jose.crypto.MACSigner
2020
import com.nimbusds.jwt.JWTClaimsSet
2121
import com.nimbusds.jwt.SignedJWT
22+
import kotlinx.coroutines.CoroutineScope
2223
import org.apache.commons.codec.digest.DigestUtils
2324
import software.amazon.awssdk.utils.UserHomeDirectoryUtils
2425
import software.aws.toolkits.core.utils.getLogger
2526
import software.aws.toolkits.core.utils.info
2627
import software.aws.toolkits.core.utils.tryDirOp
2728
import software.aws.toolkits.core.utils.warn
29+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.EncoderServer2
2830
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.extractZipFile
2931
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
3032
import software.aws.toolkits.jetbrains.settings.CodeWhispererSettings
@@ -39,19 +41,18 @@ import java.util.Base64
3941
import java.util.concurrent.atomic.AtomicInteger
4042
import javax.crypto.spec.SecretKeySpec
4143

42-
class EncoderServer(val project: Project) : Disposable {
43-
private val cachePath = Paths.get(
44+
class EncoderServer(val project: Project, private val cs: CoroutineScope) : Disposable {
45+
val cachePath = Paths.get(
4446
UserHomeDirectoryUtils.userHomeDirectory()
4547
).resolve(".aws").resolve("amazonq").resolve("cache")
4648
val manifestManager = ManifestManager()
4749
private val serverDirectoryName = "qserver-${manifestManager.currentVersion}.zip"
48-
private val numberOfRetry = AtomicInteger(0)
4950
val port by lazy { NetUtils.findAvailableSocketPort() }
5051
private val nodeRunnableName = if (manifestManager.getOs() == "windows") "node.exe" else "node"
51-
private val maxRetry: Int = 3
5252
private val key = generateHmacKey()
5353
private var processHandler: KillableProcessHandler? = null
5454
private val mapper = jacksonObjectMapper()
55+
lateinit var encoderServer2: EncoderServer2
5556

5657
fun downloadArtifactsAndStartServer() {
5758
if (ApplicationManager.getApplication().isUnitTestMode) {
@@ -92,57 +93,33 @@ class EncoderServer(val project: Project) : Disposable {
9293

9394
fun getEncryptionRequest(): String {
9495
val request = EncryptionRequest(key = Base64.getUrlEncoder().withoutPadding().encodeToString(key.encoded))
95-
return mapper.writeValueAsString(request)
96-
}
97-
98-
private fun runCommand(command: GeneralCommandLine): Boolean {
99-
try {
100-
logger.info { "starting encoder server for project context on $port for ${project.name}" }
101-
processHandler = KillableProcessHandler(command)
102-
val exitCode = processHandler?.waitFor()
103-
if (exitCode == true) {
104-
throw Exception("Encoder server exited")
105-
} else {
106-
return true
107-
}
108-
} catch (e: Exception) {
109-
logger.warn(e) { "error running encoder server:" }
110-
processHandler?.destroyProcess()
111-
numberOfRetry.incrementAndGet()
112-
return false
113-
}
96+
return mapper.writeValueAsString(request) + "\n"
11497
}
11598

11699
private fun getCommand(): GeneralCommandLine {
117100
val threadCount = CodeWhispererSettings.getInstance().getProjectContextIndexThreadCount()
118101
val isGpuEnabled = CodeWhispererSettings.getInstance().isProjectContextGpu()
119-
val map = mutableMapOf<String, String>(
120-
"PORT" to port.toString(),
121-
"START_AMAZONQ_LSP" to "true",
122-
"CACHE_DIR" to cachePath.toString(),
123-
"MODEL_DIR" to cachePath.resolve("qserver").toString()
124-
)
125-
if (threadCount > 0) {
126-
map["Q_WORKER_THREADS"] = threadCount.toString()
127-
}
128-
if (isGpuEnabled) {
129-
map["Q_ENABLE_GPU"] = "true"
102+
val environment = buildMap {
103+
if (threadCount > 0) {
104+
put("Q_WORKER_THREADS", threadCount.toString())
105+
}
106+
107+
if (isGpuEnabled) {
108+
put("Q_ENABLE_GPU", "true")
109+
}
130110
}
131-
val jsPath = cachePath.resolve("qserver").resolve("dist").resolve("extension.js").toString()
111+
112+
val jsPath = cachePath.resolve("qserver").resolve("lspServer.js").toString()
132113
val nodePath = cachePath.resolve(nodeRunnableName).toString()
133114
val command = GeneralCommandLine(nodePath, jsPath)
115+
.withParameters("--stdio")
134116
.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
135-
.withEnvironment(map)
117+
.withEnvironment(environment)
136118
return command
137119
}
138120

139121
fun start() {
140-
while (numberOfRetry.get() < maxRetry) {
141-
val isSuccess = runCommand(getCommand())
142-
if (isSuccess) {
143-
return
144-
}
145-
}
122+
encoderServer2 = EncoderServer2(this, getCommand(), project, cs)
146123
}
147124

148125
private fun close() {

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/project/ProjectContextController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import java.util.concurrent.TimeoutException
2626
@Service(Service.Level.PROJECT)
2727
class ProjectContextController(private val project: Project, private val cs: CoroutineScope) : Disposable {
2828
// TODO: Ideally we should inject dependencies via constructor for easier testing, refer to how [TelemetryService] inject publisher and batcher
29-
private val encoderServer: EncoderServer = EncoderServer(project)
29+
private val encoderServer: EncoderServer = EncoderServer(project, cs)
3030
private val projectContextProvider: ProjectContextProvider = ProjectContextProvider(project, encoderServer, cs)
3131
val initJob: Job = cs.launch {
3232
encoderServer.downloadArtifactsAndStartServer()

0 commit comments

Comments
 (0)