Skip to content

Commit 266d7d9

Browse files
committed
Merge branch 'feature/q-lsp-chat' into feature/remote-chat-lsp
2 parents 77d140c + 04aba5b commit 266d7d9

File tree

17 files changed

+214
-68
lines changed

17 files changed

+214
-68
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC
7777
val name = telemetryMap["name"] as? String ?: return
7878

7979
@Suppress("UNCHECKED_CAST")
80-
val data = telemetryMap["data"] as? Map<String, Any> ?: return
80+
val data = telemetryMap["data"] as? Map<String, Any?> ?: return
8181

8282
TelemetryService.getInstance().record(project) {
8383
datum(name) {
@@ -90,7 +90,7 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC
9090
}
9191

9292
data.forEach { (key, value) ->
93-
metadata(key, value.toString())
93+
metadata(key, value?.toString() ?: "null")
9494
}
9595
}
9696
}

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

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -372,35 +372,35 @@ private class AmazonQServerInstance(private val project: Project, private val cs
372372
}
373373

374374
val node = if (SystemInfo.isWindows) "node.exe" else "node"
375-
val cmd = GeneralCommandLine(
376-
artifact.resolve(node).toString(),
377-
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
378-
"--stdio",
379-
"--set-credentials-encryption-key",
380-
).withEnvironment(
381-
buildMap {
382-
put("NODE_EXTRA_CA_CERTS", extraCaCerts.toAbsolutePath().toString())
383-
384-
val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri)
385-
// log if only socks proxy available
386-
.firstOrNull { it.type() == Proxy.Type.HTTP }
387-
388-
if (proxy != null) {
389-
val address = proxy.address()
390-
if (address is java.net.InetSocketAddress) {
391-
put(
392-
"HTTPS_PROXY",
393-
URIBuilder("http://${address.hostName}:${address.port}").apply {
394-
val login = HttpConfigurable.getInstance().proxyLogin
395-
if (login != null) {
396-
setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword)
397-
}
398-
}.build().toASCIIString()
399-
)
375+
val cmd = NodeExePatcher.patch(artifact.resolve(node))
376+
.withParameters(
377+
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
378+
"--stdio",
379+
"--set-credentials-encryption-key",
380+
).withEnvironment(
381+
buildMap {
382+
put("NODE_EXTRA_CA_CERTS", extraCaCerts.toAbsolutePath().toString())
383+
384+
val proxy = JdkProxyProvider.getInstance().proxySelector.select(qUri)
385+
// log if only socks proxy available
386+
.firstOrNull { it.type() == Proxy.Type.HTTP }
387+
388+
if (proxy != null) {
389+
val address = proxy.address()
390+
if (address is java.net.InetSocketAddress) {
391+
put(
392+
"HTTPS_PROXY",
393+
URIBuilder("http://${address.hostName}:${address.port}").apply {
394+
val login = HttpConfigurable.getInstance().proxyLogin
395+
if (login != null) {
396+
setUserInfo(login, HttpConfigurable.getInstance().plainProxyPassword)
397+
}
398+
}.build().toASCIIString()
399+
)
400+
}
400401
}
401402
}
402-
}
403-
)
403+
)
404404
.withParentEnvironmentType(GeneralCommandLine.ParentEnvironmentType.CONSOLE)
405405

406406
launcherHandler = KillableColoredProcessHandler.Silent(cmd)
@@ -476,10 +476,18 @@ private class AmazonQServerInstance(private val project: Project, private val cs
476476
}
477477

478478
this@AmazonQServerInstance.apply {
479-
DefaultAuthCredentialsService(project, encryptionManager, this)
480-
TextDocumentServiceHandler(project, this)
481-
WorkspaceServiceHandler(project, lspInitResult, this)
482-
DefaultModuleDependenciesService(project, this)
479+
DefaultAuthCredentialsService(project, encryptionManager).also {
480+
Disposer.register(this, it)
481+
}
482+
TextDocumentServiceHandler(project).also {
483+
Disposer.register(this, it)
484+
}
485+
WorkspaceServiceHandler(project, lspInitResult).also {
486+
Disposer.register(this, it)
487+
}
488+
DefaultModuleDependenciesService(project).also {
489+
Disposer.register(this, it)
490+
}
483491
}
484492
}
485493
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp
5+
6+
import com.intellij.execution.configurations.GeneralCommandLine
7+
import software.aws.toolkits.core.utils.getLogger
8+
import software.aws.toolkits.core.utils.info
9+
import java.nio.file.Path
10+
11+
/**
12+
* Hacky nonsense to support old glibc platforms like AL2
13+
* @see "https://github.com/microsoft/vscode/issues/231623"
14+
* @see "https://github.com/aws/aws-toolkit-vscode/commit/6081f890bdbb91fcd8b60c4cc0abb65b15d4a38d"
15+
*/
16+
object NodeExePatcher {
17+
const val GLIBC_LINKER_VAR = "VSCODE_SERVER_CUSTOM_GLIBC_LINKER"
18+
const val GLIBC_PATH_VAR = "VSCODE_SERVER_CUSTOM_GLIBC_PATH"
19+
20+
fun patch(node: Path): GeneralCommandLine {
21+
val linker = System.getenv(GLIBC_LINKER_VAR)
22+
val glibc = System.getenv(GLIBC_PATH_VAR)
23+
val nodePath = node.toAbsolutePath().toString()
24+
25+
return if (!linker.isNullOrEmpty() && !glibc.isNullOrEmpty()) {
26+
GeneralCommandLine(linker)
27+
.withParameters("--library-path", glibc, nodePath)
28+
.also {
29+
getLogger<NodeExePatcher>().info { "Using glibc patch: $it" }
30+
}
31+
} else {
32+
GeneralCommandLine(nodePath)
33+
}
34+
}
35+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
7171
return localFolders
7272
.mapNotNull { localFolder ->
7373
SemVer.parseFromText(localFolder.fileName.toString())?.let { semVer ->
74-
if (semVer in manifestVersionRanges.startVersion..manifestVersionRanges.endVersion) {
74+
if (semVer >= manifestVersionRanges.startVersion && semVer < manifestVersionRanges.endVersion) {
7575
localFolder to semVer
7676
} else {
7777
null

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ class ArtifactManager @NonInjectable internal constructor(private val manifestFe
100100
SemVer.parseFromText(serverVersion)?.let { semVer ->
101101
when {
102102
version.isDelisted != false -> Pair(version, true) // Is deListed
103-
semVer in DEFAULT_VERSION_RANGE.let { it.startVersion..it.endVersion } -> Pair(version, false) // Is in range
103+
(semVer >= DEFAULT_VERSION_RANGE.startVersion && semVer < DEFAULT_VERSION_RANGE.endVersion) -> Pair(version, false) // Is in range
104104
else -> null
105105
}
106106
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ import java.util.concurrent.TimeUnit
3838
class DefaultAuthCredentialsService(
3939
private val project: Project,
4040
private val encryptionManager: JwtEncryptionManager,
41-
serverInstance: Disposable,
4241
) : AuthCredentialsService,
4342
BearerTokenProviderListener,
4443
ToolkitConnectionManagerListener,
@@ -50,7 +49,7 @@ class DefaultAuthCredentialsService(
5049
private val tokenSyncIntervalMinutes = 5L
5150

5251
init {
53-
project.messageBus.connect(serverInstance).apply {
52+
project.messageBus.connect(this).apply {
5453
subscribe(BearerTokenProviderListener.TOPIC, this@DefaultAuthCredentialsService)
5554
subscribe(ToolkitConnectionManagerListener.TOPIC, this@DefaultAuthCredentialsService)
5655
subscribe(QRegionProfileSelectedListener.TOPIC, this@DefaultAuthCredentialsService)

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,11 @@ import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
1515

1616
class DefaultModuleDependenciesService(
1717
private val project: Project,
18-
serverInstance: Disposable,
1918
) : ModuleDependenciesService,
20-
ModuleRootListener {
21-
19+
ModuleRootListener,
20+
Disposable {
2221
init {
23-
project.messageBus.connect(serverInstance).subscribe(
22+
project.messageBus.connect(this).subscribe(
2423
ModuleRootListener.TOPIC,
2524
this
2625
)
@@ -52,4 +51,7 @@ class DefaultModuleDependenciesService(
5251
}
5352
}
5453
}
54+
55+
override fun dispose() {
56+
}
5557
}

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import org.eclipse.lsp4j.DidChangeTextDocumentParams
2222
import org.eclipse.lsp4j.DidCloseTextDocumentParams
2323
import org.eclipse.lsp4j.DidOpenTextDocumentParams
2424
import org.eclipse.lsp4j.DidSaveTextDocumentParams
25+
import org.eclipse.lsp4j.Position
26+
import org.eclipse.lsp4j.Range
2527
import org.eclipse.lsp4j.TextDocumentContentChangeEvent
2628
import org.eclipse.lsp4j.TextDocumentIdentifier
2729
import org.eclipse.lsp4j.TextDocumentItem
@@ -32,27 +34,26 @@ import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
3234

3335
class TextDocumentServiceHandler(
3436
private val project: Project,
35-
private val serverInstance: Disposable,
3637
) : FileDocumentManagerListener,
3738
FileEditorManagerListener,
3839
BulkFileListener,
39-
DocumentListener {
40-
40+
DocumentListener,
41+
Disposable {
4142
init {
4243
// didOpen & didClose events
43-
project.messageBus.connect(serverInstance).subscribe(
44+
project.messageBus.connect(this).subscribe(
4445
FileEditorManagerListener.FILE_EDITOR_MANAGER,
4546
this
4647
)
4748

4849
// didChange events
49-
project.messageBus.connect(serverInstance).subscribe(
50+
project.messageBus.connect(this).subscribe(
5051
VirtualFileManager.VFS_CHANGES,
5152
this
5253
)
5354

5455
// didSave events
55-
project.messageBus.connect(serverInstance).subscribe(
56+
project.messageBus.connect(this).subscribe(
5657
FileDocumentManagerListener.TOPIC,
5758
this
5859
)
@@ -72,7 +73,7 @@ class TextDocumentServiceHandler(
7273
realTimeEdit(event)
7374
}
7475
},
75-
serverInstance
76+
this
7677
)
7778
}
7879
AmazonQLspService.executeIfRunning(project) { languageServer ->
@@ -165,6 +166,9 @@ class TextDocumentServiceHandler(
165166
pluginAwareExecuteOnPooledThread {
166167
val vFile = FileDocumentManager.getInstance().getFile(event.document) ?: return@pluginAwareExecuteOnPooledThread
167168
toUriString(vFile)?.let { uri ->
169+
val editor = FileEditorManager.getInstance(project).selectedTextEditor ?: return@pluginAwareExecuteOnPooledThread
170+
val logicalPosition = editor.offsetToLogicalPosition(event.offset)
171+
val newLogicalPosition = editor.offsetToLogicalPosition(event.offset + event.newLength)
168172
languageServer.textDocumentService.didChange(
169173
DidChangeTextDocumentParams().apply {
170174
textDocument = VersionedTextDocumentIdentifier().apply {
@@ -174,6 +178,10 @@ class TextDocumentServiceHandler(
174178
contentChanges = listOf(
175179
TextDocumentContentChangeEvent().apply {
176180
text = event.newFragment.toString()
181+
range = Range(
182+
Position(logicalPosition.line, logicalPosition.column),
183+
Position(newLogicalPosition.line, newLogicalPosition.column)
184+
)
177185
}
178186
)
179187
}
@@ -183,4 +191,7 @@ class TextDocumentServiceHandler(
183191
}
184192
// Process document changes here
185193
}
194+
195+
override fun dispose() {
196+
}
186197
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,22 +45,22 @@ import java.nio.file.Paths
4545
class WorkspaceServiceHandler(
4646
private val project: Project,
4747
initializeResult: InitializeResult,
48-
serverInstance: Disposable,
4948
) : BulkFileListener,
50-
ModuleRootListener {
49+
ModuleRootListener,
50+
Disposable {
5151

5252
private var lastSnapshot: List<WorkspaceFolder> = emptyList()
5353
private val operationMatchers: MutableMap<FileOperationType, List<Pair<PathMatcher, String>>> = mutableMapOf()
5454

5555
init {
5656
operationMatchers.putAll(initializePatterns(initializeResult))
5757

58-
project.messageBus.connect(serverInstance).subscribe(
58+
project.messageBus.connect(this).subscribe(
5959
VirtualFileManager.VFS_CHANGES,
6060
this
6161
)
6262

63-
project.messageBus.connect(serverInstance).subscribe(
63+
project.messageBus.connect(this).subscribe(
6464
ModuleRootListener.TOPIC,
6565
this
6666
)
@@ -313,4 +313,7 @@ class WorkspaceServiceHandler(
313313
lastSnapshot = currentSnapshot
314314
}
315315
}
316+
317+
override fun dispose() {
318+
}
316319
}

plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImplTest.kt

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,36 @@ class AmazonQLanguageClientImplTest {
8181
)
8282
}
8383

84+
@Test
85+
fun `telemetryEvent handles null`() {
86+
val telemetryService = mockk<TelemetryService>(relaxed = true)
87+
mockkObject(TelemetryService)
88+
every { TelemetryService.getInstance() } returns telemetryService
89+
90+
val builderCaptor = slot<MetricEvent.Builder.() -> Unit>()
91+
every { telemetryService.record(project, capture(builderCaptor)) } returns Unit
92+
93+
val event = mapOf(
94+
"name" to "test_event",
95+
"data" to mapOf(
96+
"key1" to null,
97+
)
98+
)
99+
100+
sut.telemetryEvent(event)
101+
102+
val builder = DefaultMetricEvent.builder()
103+
builderCaptor.captured.invoke(builder)
104+
105+
val metricEvent = builder.build()
106+
val datum = metricEvent.data.first()
107+
108+
assertThat(datum.name).isEqualTo("test_event")
109+
assertThat(datum.metadata).contains(
110+
entry("key1", "null"),
111+
)
112+
}
113+
84114
@Test
85115
fun `telemetryEvent handles event with result field`() {
86116
val telemetryService = mockk<TelemetryService>(relaxed = true)

0 commit comments

Comments
 (0)