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
Changes from 7 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,78 @@ 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 kotlinx.serialization.Serializable
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.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

@Serializable
data class ExtendedClientMetadata(
val aws: AwsMetadata,
)

@Serializable
data class AwsMetadata(
val clientInfo: ClientInfoMetadata,
)

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

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

private 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
)
)
)
}

// 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 +124,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 +134,60 @@ class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disp
private val launcherFuture: Future<Void>
private val launcherHandler: KillableProcessHandler

private fun createClientCapabilities(): ClientCapabilities {
return 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> {
return project.basePath?.let { basePath ->
Copy link
Contributor

Choose a reason for hiding this comment

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

add a note to handle the case where this is null

listOf(
WorkspaceFolder(
URI("file://$basePath").toString(),
project.name
)
)
} ?: emptyList() // 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 {
return 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 +224,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
Loading