Skip to content

Commit 175b13a

Browse files
Merge branch 'feature/q-lsp' into samgst/q-lsp-TDmessages
2 parents b69d6c6 + e20697a commit 175b13a

File tree

25 files changed

+1517
-96
lines changed

25 files changed

+1517
-96
lines changed

buildspec/linuxUiTests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ phases:
4747
- chmod +x gradlew
4848

4949
- ffmpeg -loglevel quiet -nostdin -f x11grab -video_size ${SCREEN_WIDTH}x${SCREEN_HEIGHT} -i ${DISPLAY} -codec:v libx264 -pix_fmt yuv420p -vf drawtext="fontsize=48:box=1:[email protected]:boxborderw=5:fontcolor=white:x=0:y=h-text_h:text='%{gmtime\:%H\\\\\:%M\\\\\:%S}'" -framerate 12 -g 12 /tmp/screen_recording.mp4 &
50-
- ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME :ui-tests-starter:test coverageReport --console plain --info
50+
- ./gradlew -PideProfileName=$ALTERNATIVE_IDE_PROFILE_NAME :ui-tests-starter:uiTest coverageReport --console plain --info
5151

5252
post_build:
5353
commands:

gradle/libs.versions.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref =
106106
kotlin-stdLibJdk8 = { module = "org.jetbrains.kotlin:kotlin-stdlib-jdk8", version.ref = "kotlin" }
107107
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" }
108108
mockito-core = { module = "org.mockito:mockito-core", version.ref = "mockito" }
109+
mockito-junit-jupiter = { module = "org.mockito:mockito-junit-jupiter", version.ref = "mockito" }
109110
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
110111
mockk = { module = "io.mockk:mockk", version.ref="mockk" }
111112
nimbus-jose-jwt = {module = "com.nimbusds:nimbus-jose-jwt", version.ref = "nimbus-jose-jwt"}
@@ -121,7 +122,7 @@ zjsonpatch = { module = "com.flipkart.zjsonpatch:zjsonpatch", version.ref = "zjs
121122
[bundles]
122123
jackson = ["jackson-datetime", "jackson-kotlin", "jackson-yaml", "jackson-xml"]
123124
kotlin = ["kotlin-stdLibJdk8", "kotlin-reflect"]
124-
mockito = ["mockito-core", "mockito-kotlin"]
125+
mockito = ["mockito-core", "mockito-junit-jupiter", "mockito-kotlin"]
125126
sshd = ["sshd-core", "sshd-scp", "sshd-sftp"]
126127

127128
[plugins]

noop/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@
33

44
// project that does nothing
55
tasks.register("test")
6+
tasks.register("uiTest")

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

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@ import org.eclipse.lsp4j.InitializedParams
3737
import org.eclipse.lsp4j.SynchronizationCapabilities
3838
import org.eclipse.lsp4j.TextDocumentClientCapabilities
3939
import org.eclipse.lsp4j.WorkspaceClientCapabilities
40-
import org.eclipse.lsp4j.WorkspaceFolder
4140
import org.eclipse.lsp4j.jsonrpc.Launcher
4241
import org.eclipse.lsp4j.launch.LSPLauncher
4342
import org.slf4j.event.Level
@@ -49,14 +48,15 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCred
4948
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
5049
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.createExtendedClientMetadata
5150
import software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument.TextDocumentServiceHandler
51+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
52+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
5253
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
5354
import java.io.IOException
5455
import java.io.OutputStreamWriter
5556
import java.io.PipedInputStream
5657
import java.io.PipedOutputStream
5758
import java.io.PrintWriter
5859
import java.io.StringWriter
59-
import java.net.URI
6060
import java.nio.charset.StandardCharsets
6161
import java.util.concurrent.Future
6262
import kotlin.time.Duration.Companion.seconds
@@ -212,21 +212,11 @@ private class AmazonQServerInstance(private val project: Project, private val cs
212212
fileOperations = FileOperationsWorkspaceCapabilities().apply {
213213
didCreate = true
214214
didDelete = true
215+
didRename = true
215216
}
216217
}
217218
}
218219

219-
// needs case handling when project's base path is null: default projects/unit tests
220-
private fun createWorkspaceFolders(): List<WorkspaceFolder> =
221-
project.basePath?.let { basePath ->
222-
listOf(
223-
WorkspaceFolder(
224-
URI("file://$basePath").toString(),
225-
project.name
226-
)
227-
)
228-
}.orEmpty() // no folders to report or workspace not folder based
229-
230220
private fun createClientInfo(): ClientInfo {
231221
val metadata = ClientMetadata.getDefault()
232222
return ClientInfo().apply {
@@ -240,7 +230,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
240230
processId = ProcessHandle.current().pid().toInt()
241231
capabilities = createClientCapabilities()
242232
clientInfo = createClientInfo()
243-
workspaceFolders = createWorkspaceFolders()
233+
workspaceFolders = createWorkspaceFolders(project)
244234
initializationOptions = createExtendedClientMetadata()
245235
}
246236

@@ -308,6 +298,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs
308298

309299
DefaultAuthCredentialsService(project, encryptionManager, this)
310300
TextDocumentServiceHandler(project, this)
301+
WorkspaceServiceHandler(project, this)
311302
}
312303

313304
override fun dispose() {

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,11 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
55

6+
import com.intellij.openapi.project.Project
7+
import com.intellij.platform.ide.progress.withBackgroundProgress
68
import com.intellij.util.io.createDirectories
79
import com.intellij.util.text.SemVer
10+
import kotlinx.coroutines.CancellationException
811
import org.jetbrains.annotations.VisibleForTesting
912
import software.aws.toolkits.core.utils.deleteIfExists
1013
import software.aws.toolkits.core.utils.error
@@ -14,6 +17,7 @@ import software.aws.toolkits.core.utils.info
1417
import software.aws.toolkits.core.utils.warn
1518
import software.aws.toolkits.jetbrains.core.saveFileFromUrl
1619
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
20+
import software.aws.toolkits.resources.AwsCoreBundle
1721
import java.nio.file.Path
1822
import java.util.concurrent.atomic.AtomicInteger
1923

@@ -99,7 +103,7 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
99103
return !hasInvalidFiles
100104
}
101105

102-
fun tryDownloadLspArtifacts(versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?) {
106+
suspend fun tryDownloadLspArtifacts(project: Project, versions: List<ManifestManager.Version>, target: ManifestManager.VersionTarget?): Path? {
103107
val temporaryDownloadPath = lspArtifactsPath.resolve("temp")
104108
val downloadPath = lspArtifactsPath.resolve(versions.first().serverVersion.toString())
105109

@@ -108,23 +112,37 @@ class ArtifactHelper(private val lspArtifactsPath: Path = DEFAULT_ARTIFACT_PATH,
108112
logger.info { "Attempt ${currentAttempt.get()} of $maxDownloadAttempts to download LSP artifacts" }
109113

110114
try {
111-
if (downloadLspArtifacts(temporaryDownloadPath, target) && target != null && !target.contents.isNullOrEmpty()) {
112-
moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath)
113-
target.contents
114-
.mapNotNull { it.filename }
115-
.forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) }
116-
logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" }
117-
return
115+
return withBackgroundProgress(
116+
project,
117+
AwsCoreBundle.message("amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts"),
118+
cancellable = true
119+
) {
120+
if (downloadLspArtifacts(temporaryDownloadPath, target) && target != null && !target.contents.isNullOrEmpty()) {
121+
moveFilesFromSourceToDestination(temporaryDownloadPath, downloadPath)
122+
target.contents
123+
.mapNotNull { it.filename }
124+
.forEach { filename -> extractZipFile(downloadPath.resolve(filename), downloadPath) }
125+
logger.info { "Successfully downloaded and moved LSP artifacts to $downloadPath" }
126+
127+
return@withBackgroundProgress downloadPath
128+
}
129+
130+
return@withBackgroundProgress null
118131
}
119132
} catch (e: Exception) {
120-
logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" }
133+
when (e) {
134+
is CancellationException -> {
135+
logger.error(e) { "User cancelled download and extracting of LSP artifacts.." }
136+
currentAttempt.set(maxDownloadAttempts) // To exit the while loop.
137+
}
138+
else -> { logger.error(e) { "Failed to download/move LSP artifacts on attempt ${currentAttempt.get()}" } }
139+
}
121140
temporaryDownloadPath.toFile().deleteRecursively()
122141
downloadPath.toFile().deleteRecursively()
123142
}
124143
}
125-
if (currentAttempt.get() >= maxDownloadAttempts) {
126-
throw LspException("Failed to download LSP artifacts after $maxDownloadAttempts attempts", LspException.ErrorCode.DOWNLOAD_FAILED)
127-
}
144+
logger.error { "Failed to download LSP artifacts after $maxDownloadAttempts attempts" }
145+
return null
128146
}
129147

130148
@VisibleForTesting

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

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,17 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts
55

6+
import com.intellij.openapi.project.Project
67
import com.intellij.util.text.SemVer
78
import org.jetbrains.annotations.VisibleForTesting
89
import software.aws.toolkits.core.utils.error
910
import software.aws.toolkits.core.utils.getLogger
1011
import software.aws.toolkits.core.utils.info
1112
import software.aws.toolkits.jetbrains.services.amazonq.project.manifest.ManifestManager
13+
import java.nio.file.Path
1214

1315
class ArtifactManager(
16+
private val project: Project,
1417
private val manifestFetcher: ManifestFetcher = ManifestFetcher(),
1518
private val artifactHelper: ArtifactHelper = ArtifactHelper(),
1619
manifestRange: SupportedManifestVersionRange?,
@@ -27,9 +30,6 @@ class ArtifactManager(
2730

2831
private val manifestVersionRanges: SupportedManifestVersionRange = manifestRange ?: DEFAULT_VERSION_RANGE
2932

30-
// Secondary constructor with no parameters
31-
constructor() : this(ManifestFetcher(), ArtifactHelper(), null)
32-
3333
companion object {
3434
private val DEFAULT_VERSION_RANGE = SupportedManifestVersionRange(
3535
startVersion = SemVer("3.0.0", 3, 0, 0),
@@ -38,7 +38,7 @@ class ArtifactManager(
3838
private val logger = getLogger<ArtifactManager>()
3939
}
4040

41-
fun fetchArtifact() {
41+
suspend fun fetchArtifact(): Path {
4242
val manifest = manifestFetcher.fetch() ?: throw LspException(
4343
"Language Support is not available, as manifest is missing.",
4444
LspException.ErrorCode.MANIFEST_FETCH_FAILED
@@ -51,20 +51,22 @@ class ArtifactManager(
5151
// No versions are found which are in the given range. Fallback to local lsp artifacts.
5252
val localLspArtifacts = this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges)
5353
if (localLspArtifacts.isNotEmpty()) {
54-
return
54+
return localLspArtifacts.first().first
5555
}
5656
throw LspException("Language server versions not found in manifest.", LspException.ErrorCode.NO_COMPATIBLE_LSP_VERSION)
5757
}
5858

5959
// If there is an LSP Manifest with the same version
6060
val target = getTargetFromLspManifest(lspVersions.inRangeVersions)
61-
6261
// Get Local LSP files and check if we can re-use existing LSP Artifacts
63-
if (!this.artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
64-
this.artifactHelper.tryDownloadLspArtifacts(lspVersions.inRangeVersions, target)
62+
val artifactPath: Path = if (this.artifactHelper.getExistingLspArtifacts(lspVersions.inRangeVersions, target)) {
63+
this.artifactHelper.getAllLocalLspArtifactsWithinManifestRange(manifestVersionRanges).first().first
64+
} else {
65+
this.artifactHelper.tryDownloadLspArtifacts(project, lspVersions.inRangeVersions, target)
66+
?: throw LspException("Failed to download LSP artifacts", LspException.ErrorCode.DOWNLOAD_FAILED)
6567
}
66-
6768
this.artifactHelper.deleteOlderLspArtifacts(manifestVersionRanges)
69+
return artifactPath
6870
}
6971

7072
@VisibleForTesting

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import java.nio.file.Path
1919
class ManifestFetcher(
2020
private val lspManifestUrl: String = DEFAULT_MANIFEST_URL,
2121
private val manifestManager: ManifestManager = ManifestManager(),
22-
private val lspManifestFilePath: Path = DEFAULT_MANIFEST_PATH,
22+
private val manifestPath: Path = DEFAULT_MANIFEST_PATH,
2323
) {
2424
companion object {
2525
private val logger = getLogger<ManifestFetcher>()
@@ -34,6 +34,10 @@ class ManifestFetcher(
3434
.resolve("jetbrains-lsp-manifest.json")
3535
}
3636

37+
@get:VisibleForTesting
38+
internal val lspManifestFilePath: Path
39+
get() = manifestPath
40+
3741
/**
3842
* Method which will be used to fetch latest manifest.
3943
* */
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.util
5+
6+
import com.intellij.openapi.vfs.VfsUtilCore
7+
import com.intellij.openapi.vfs.VirtualFile
8+
import software.aws.toolkits.core.utils.getLogger
9+
import software.aws.toolkits.core.utils.warn
10+
import java.io.File
11+
import java.net.URI
12+
import java.net.URISyntaxException
13+
14+
object FileUriUtil {
15+
16+
fun toUriString(virtualFile: VirtualFile): String? {
17+
val protocol = virtualFile.fileSystem.protocol
18+
val uri = when (protocol) {
19+
"jar" -> VfsUtilCore.convertToURL(virtualFile.url)?.toExternalForm()
20+
"jrt" -> virtualFile.url
21+
else -> toUri(VfsUtilCore.virtualToIoFile(virtualFile)).toASCIIString()
22+
} ?: return null
23+
24+
return if (virtualFile.isDirectory) {
25+
uri.trimEnd('/', '\\')
26+
} else {
27+
uri
28+
}
29+
}
30+
31+
private fun toUri(file: File): URI {
32+
try {
33+
// URI scheme specified by language server protocol
34+
return URI("file", "", file.absoluteFile.toURI().path, null)
35+
} catch (e: URISyntaxException) {
36+
LOG.warn { "${e.localizedMessage}: $e" }
37+
return file.absoluteFile.toURI()
38+
}
39+
}
40+
41+
private val LOG = getLogger<FileUriUtil>()
42+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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.util
5+
6+
import com.intellij.openapi.project.Project
7+
import com.intellij.openapi.roots.ProjectRootManager
8+
import org.eclipse.lsp4j.WorkspaceFolder
9+
10+
object WorkspaceFolderUtil {
11+
fun createWorkspaceFolders(project: Project): List<WorkspaceFolder> =
12+
if (project.isDefault) {
13+
emptyList()
14+
} else {
15+
ProjectRootManager.getInstance(project).contentRoots.map { contentRoot ->
16+
WorkspaceFolder().apply {
17+
name = contentRoot.name
18+
this.uri = contentRoot.url
19+
}
20+
}
21+
}
22+
}

0 commit comments

Comments
 (0)