-
Notifications
You must be signed in to change notification settings - Fork 260
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
Changes from 7 commits
1733ef8
139af94
1670212
add8d41
ee0b589
0819535
0d0494e
2e3ad80
30d7c9c
ec614b8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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( | ||
samgst-amazon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 { | ||
|
@@ -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 | ||
|
@@ -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 -> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
||
|
@@ -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)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. where does this 30 come from? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
Uh oh!
There was an error while loading. Please reload this page.