@@ -15,25 +15,40 @@ import com.intellij.openapi.Disposable
1515import com.intellij.openapi.components.Service
1616import com.intellij.openapi.components.service
1717import com.intellij.openapi.project.Project
18+ import com.intellij.openapi.util.Disposer
1819import com.intellij.openapi.util.Key
1920import com.intellij.util.io.await
2021import kotlinx.coroutines.CoroutineScope
22+ import kotlinx.coroutines.TimeoutCancellationException
2123import kotlinx.coroutines.launch
24+ import kotlinx.coroutines.time.withTimeout
25+ import org.eclipse.lsp4j.ClientCapabilities
26+ import org.eclipse.lsp4j.ClientInfo
27+ import org.eclipse.lsp4j.FileOperationsWorkspaceCapabilities
2228import org.eclipse.lsp4j.InitializeParams
2329import org.eclipse.lsp4j.InitializedParams
30+ import org.eclipse.lsp4j.SynchronizationCapabilities
31+ import org.eclipse.lsp4j.TextDocumentClientCapabilities
32+ import org.eclipse.lsp4j.WorkspaceClientCapabilities
33+ import org.eclipse.lsp4j.WorkspaceFolder
2434import org.eclipse.lsp4j.jsonrpc.Launcher
2535import org.eclipse.lsp4j.launch.LSPLauncher
2636import org.slf4j.event.Level
2737import software.aws.toolkits.core.utils.getLogger
2838import software.aws.toolkits.core.utils.warn
2939import software.aws.toolkits.jetbrains.isDeveloperMode
40+ import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
41+ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtendedClientMetadata
42+ import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
3043import java.io.IOException
3144import java.io.OutputStreamWriter
3245import java.io.PipedInputStream
3346import java.io.PipedOutputStream
3447import java.io.PrintWriter
3548import java.io.StringWriter
49+ import java.net.URI
3650import java.nio.charset.StandardCharsets
51+ import java.time.Duration
3752import java.util.concurrent.Future
3853
3954// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
@@ -70,7 +85,35 @@ internal class LSPProcessListener : ProcessListener {
7085}
7186
7287@Service(Service .Level .PROJECT )
73- class AmazonQLspService (project : Project , private val cs : CoroutineScope ) : Disposable {
88+ class AmazonQLspService (private val project : Project , private val cs : CoroutineScope ) : Disposable {
89+ private var instance: AmazonQServerInstance ? = null
90+
91+ init {
92+ cs.launch {
93+ // manage lifecycle RAII-like so we can restart at arbitrary time
94+ // and suppress IDE error if server fails to start
95+ try {
96+ instance = AmazonQServerInstance (project, cs).also {
97+ Disposer .register(this @AmazonQLspService, it)
98+ }
99+ } catch (e: Exception ) {
100+ LOG .warn(e) { " Failed to start LSP server" }
101+ }
102+ }
103+ }
104+
105+ override fun dispose () {
106+ }
107+
108+ companion object {
109+ private val LOG = getLogger<AmazonQLspService >()
110+ fun getInstance (project : Project ) = project.service<AmazonQLspService >()
111+ }
112+ }
113+
114+ private class AmazonQServerInstance (private val project : Project , private val cs : CoroutineScope ) : Disposable {
115+ private val encryptionManager = JwtEncryptionManager ()
116+
74117 private val launcher: Launcher <AmazonQLanguageServer >
75118
76119 private val languageServer: AmazonQLanguageServer
@@ -80,8 +123,63 @@ class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disp
80123 private val launcherFuture: Future <Void >
81124 private val launcherHandler: KillableProcessHandler
82125
126+ private fun createClientCapabilities (): ClientCapabilities =
127+ ClientCapabilities ().apply {
128+ textDocument = TextDocumentClientCapabilities ().apply {
129+ // For didSaveTextDocument, other textDocument/ messages always mandatory
130+ synchronization = SynchronizationCapabilities ().apply {
131+ didSave = true
132+ }
133+ }
134+
135+ workspace = WorkspaceClientCapabilities ().apply {
136+ applyEdit = false
137+
138+ // For workspace folder changes
139+ workspaceFolders = true
140+
141+ // For file operations (create, delete)
142+ fileOperations = FileOperationsWorkspaceCapabilities ().apply {
143+ didCreate = true
144+ didDelete = true
145+ }
146+ }
147+ }
148+
149+ // needs case handling when project's base path is null: default projects/unit tests
150+ private fun createWorkspaceFolders (): List <WorkspaceFolder > =
151+ project.basePath?.let { basePath ->
152+ listOf (
153+ WorkspaceFolder (
154+ URI (" file://$basePath " ).toString(),
155+ project.name
156+ )
157+ )
158+ }.orEmpty() // no folders to report or workspace not folder based
159+
160+ private fun createClientInfo (): ClientInfo {
161+ val metadata = ClientMetadata .getDefault()
162+ return ClientInfo ().apply {
163+ name = metadata.awsProduct.toString()
164+ version = metadata.awsVersion
165+ }
166+ }
167+
168+ private fun createInitializeParams (): InitializeParams =
169+ InitializeParams ().apply {
170+ processId = ProcessHandle .current().pid().toInt()
171+ capabilities = createClientCapabilities()
172+ clientInfo = createClientInfo()
173+ workspaceFolders = createWorkspaceFolders()
174+ initializationOptions = createExtendedClientMetadata()
175+ }
176+
83177 init {
84- val cmd = GeneralCommandLine (" amazon-q-lsp" )
178+ val cmd = GeneralCommandLine (
179+ " amazon-q-lsp" ,
180+ " --stdio" ,
181+ " --set-credentials-encryption-key" ,
182+ )
85183
86184 launcherHandler = KillableColoredProcessHandler .Silent (cmd)
87185 val inputWrapper = LSPProcessListener ()
@@ -116,18 +214,17 @@ class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disp
116214 launcherFuture = launcher.startListening()
117215
118216 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?
217+ // encryption info must be sent within 5s or Flare process will exit
218+ encryptionManager.writeInitializationPayload(launcherHandler.process.outputStream)
219+
220+ val initializeResult = try {
221+ withTimeout(Duration .ofSeconds(10 )) {
222+ languageServer.initialize(createInitializeParams()).await()
128223 }
129- // probably need a timeout
130- ).await()
224+ } catch (_: TimeoutCancellationException ) {
225+ LOG .warn { " LSP initialization timed out" }
226+ null
227+ }
131228
132229 // then if this succeeds then we can allow the client to send requests
133230 if (initializeResult == null ) {
@@ -154,7 +251,6 @@ class AmazonQLspService(project: Project, private val cs: CoroutineScope) : Disp
154251 }
155252
156253 companion object {
157- private val LOG = getLogger<AmazonQLspService >()
158- fun getInstance (project : Project ) = project.service<AmazonQLspService >()
254+ private val LOG = getLogger<AmazonQServerInstance >()
159255 }
160256}
0 commit comments