Skip to content

feat(amazonQ): LSP -- Implement Initialize message #5367

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Feb 13, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,36 @@ 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.TimeoutCancellationException
import kotlinx.coroutines.launch
import kotlinx.coroutines.time.withTimeout
import org.eclipse.lsp4j.ClientCapabilities
import org.eclipse.lsp4j.ClientInfo
import org.eclipse.lsp4j.FileOperationsWorkspaceCapabilities
import org.eclipse.lsp4j.InitializeParams
import org.eclipse.lsp4j.InitializedParams
import org.eclipse.lsp4j.SynchronizationCapabilities
import org.eclipse.lsp4j.TextDocumentClientCapabilities
import org.eclipse.lsp4j.WorkspaceClientCapabilities
import org.eclipse.lsp4j.WorkspaceFolder
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 software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtendedClientMetadata
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
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.net.URI
import java.nio.charset.StandardCharsets
import java.time.Duration
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 {
Expand Down Expand Up @@ -70,7 +82,7 @@ internal class LSPProcessListener : ProcessListener {
}

@Service(Service.Level.PROJECT)
class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disposable {
class AmazonQLspService(private val project: Project, private val cs: CoroutineScope) : Disposable {
private val launcher: Launcher<AmazonQLanguageServer>

private val languageServer: AmazonQLanguageServer
Expand All @@ -80,6 +92,57 @@ class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disp
private val launcherFuture: Future<Void>
private val launcherHandler: KillableProcessHandler

private fun createClientCapabilities(): ClientCapabilities =
ClientCapabilities().apply {
textDocument = TextDocumentClientCapabilities().apply {
// For didSaveTextDocument, other textDocument/ messages always mandatory
synchronization = SynchronizationCapabilities().apply {
didSave = true
}
}

workspace = WorkspaceClientCapabilities().apply {
applyEdit = false

// For workspace folder changes
workspaceFolders = true

// For file operations (create, delete)
fileOperations = FileOperationsWorkspaceCapabilities().apply {
didCreate = true
didDelete = true
}
}
}

// needs case handling when project's base path is null: default projects/unit tests
private fun createWorkspaceFolders(): List<WorkspaceFolder> =
project.basePath?.let { basePath ->
listOf(
WorkspaceFolder(
URI("file://$basePath").toString(),
project.name
)
)
}.orEmpty() // no folders to report or workspace not folder based

private fun createClientInfo(): ClientInfo {
val metadata = ClientMetadata.getDefault()
return ClientInfo().apply {
name = metadata.awsProduct.toString()
version = metadata.awsVersion
}
}

private fun createInitializeParams(): InitializeParams =
InitializeParams().apply {
processId = ProcessHandle.current().pid().toInt()
capabilities = createClientCapabilities()
clientInfo = createClientInfo()
workspaceFolders = createWorkspaceFolders()
initializationOptions = createExtendedClientMetadata()
}

init {
val cmd = GeneralCommandLine("amazon-q-lsp")

Expand Down Expand Up @@ -116,18 +179,14 @@ class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disp
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?
val initializeResult = try {
withTimeout(Duration.ofSeconds(30)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does this 30 come from?

Copy link
Contributor Author

@samgst-amazon samgst-amazon Feb 12, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

arbitrary -- saw your //comment about having a timeout and thought we should have something

can change to retry with backoff pattern or something else, just wasn't sure what was appropriate here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5-10s is probably good enough. if it doesn't spin up quickly we should kill server and retry, then fail after some # of attempts. can you log that as a separate task on the board?

languageServer.initialize(createInitializeParams()).await()
}
// probably need a timeout
).await()
} catch (e: TimeoutCancellationException) {
LOG.warn { "LSP initialization timed out" }
null
}

// then if this succeeds then we can allow the client to send requests
if (initializeResult == null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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

import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata

data class ExtendedClientMetadata(
val aws: AwsMetadata,
)

data class AwsMetadata(
val clientInfo: ClientInfoMetadata,
)

data class ClientInfoMetadata(
val extension: ExtensionMetadata,
val clientId: String,
val version: String,
val name: String,
)

data class ExtensionMetadata(
val name: String,
val version: String,
)

fun createExtendedClientMetadata(): ExtendedClientMetadata {
val metadata = ClientMetadata.getDefault()
return ExtendedClientMetadata(
aws = AwsMetadata(
clientInfo = ClientInfoMetadata(
extension = ExtensionMetadata(
name = metadata.awsProduct.toString(),
version = metadata.awsVersion
),
clientId = metadata.clientId,
version = metadata.parentProductVersion,
name = metadata.parentProduct
)
)
)
}
Loading