Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@

textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor)
.bindText(
{ LspSettings.getInstance().getArtifactPath() },
{ LspSettings.getInstance().setArtifactPath(it.takeIf { v -> v.isNotBlank() }) }
{ LspSettings.getInstance().getArtifactPath().orEmpty() },
{ LspSettings.getInstance().setArtifactPath(it) }

Check warning on line 76 in plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt#L76

Added line #L76 was not covered by tests
)
.applyToComponent {
emptyText.text = "Choose a file to upload"
emptyText.text = message("executableCommon.auto_managed")

Check warning on line 79 in plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/codewhisperer/jetbrains-community/src/software/aws/toolkits/jetbrains/services/codewhisperer/settings/CodeWhispererConfigurable.kt#L79

Added line #L79 was not covered by tests
}
.resizableColumn()
.align(Align.FILL)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.util.io.await
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
Expand Down Expand Up @@ -45,6 +46,7 @@
import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.isDeveloperMode
import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager
import software.aws.toolkits.jetbrains.services.amazonq.lsp.auth.DefaultAuthCredentialsService
import software.aws.toolkits.jetbrains.services.amazonq.lsp.dependencies.DefaultModuleDependenciesService
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
Expand All @@ -53,6 +55,7 @@
import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
import software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace.WorkspaceServiceHandler
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
import software.aws.toolkits.jetbrains.settings.LspSettings
import java.io.IOException
import java.io.OutputStreamWriter
import java.io.PipedInputStream
Expand Down Expand Up @@ -245,8 +248,12 @@
}

init {
// will cause slow service init, but maybe fine for now. will not block UI since fetch/extract will be under background progress
val artifact = runBlocking { ArtifactManager(project, manifestRange = null).fetchArtifact() }.toAbsolutePath()

Check warning on line 252 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L252 was not covered by tests
val node = if (SystemInfo.isWindows) "node.exe" else "node"
val cmd = GeneralCommandLine(
"amazon-q-lsp",
artifact.resolve(node).toString(),

Check warning on line 255 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L255 was not covered by tests
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
"--stdio",
"--set-credentials-encryption-key",
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
import com.intellij.openapi.util.text.StringUtil
import com.intellij.util.io.DigestUtil
import com.intellij.util.system.CpuArch
import software.aws.toolkits.core.utils.ZIP_PROPERTY_POSIX
import software.aws.toolkits.core.utils.createParentDirectories
import software.aws.toolkits.core.utils.exists
import software.aws.toolkits.core.utils.hasPosixFilePermissions
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.net.URI
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.util.zip.ZipFile
import kotlin.io.path.isDirectory
import kotlin.io.path.listDirectoryEntries

Expand Down Expand Up @@ -78,17 +80,20 @@
}

try {
ZipFile(zipFilePath.toFile()).use { zipFile ->
zipFile.entries()
.asSequence()
.filterNot { it.isDirectory }
.map { zipEntry ->
val destPath = destDir.resolve(zipEntry.name)
destPath.createParentDirectories()
FileOutputStream(destPath.toFile()).use { targetFile ->
zipFile.getInputStream(zipEntry).copyTo(targetFile)
FileSystems.newFileSystem(

Check warning on line 83 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L83 was not covered by tests
// jar prefix due to potentially ambiguous resolution to wrong fs impl for zipfs on windows
URI("jar:${zipFilePath.toUri()}"),
mapOf(ZIP_PROPERTY_POSIX to destDir.hasPosixFilePermissions())
).use { zipfs ->
Files.walk(zipfs.getPath("/")).use { paths ->
paths

Check warning on line 89 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt#L85-L89

Added lines #L85 - L89 were not covered by tests
.filter { !it.isDirectory() }
.forEach { zipEntry ->
val destPath = Paths.get(destDir.toString(), zipEntry.toString())
destPath.createParentDirectories()
Files.copy(zipEntry, destPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES)

Check warning on line 94 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt#L91-L94

Added lines #L91 - L94 were not covered by tests
}
}.toList()
}

Check warning on line 96 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/artifacts/LspUtils.kt

View check run for this annotation

Codecov / codecov/patch

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

Added line #L96 was not covered by tests
}
} catch (e: Exception) {
throw LspException("Failed to extract zip file: ${e.message}", LspException.ErrorCode.UNZIP_FAILED, cause = e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import com.intellij.openapi.components.State
import com.intellij.openapi.components.Storage
import com.intellij.openapi.components.service
import com.intellij.util.xmlb.annotations.Property
import com.intellij.util.text.nullize

@Service
@State(name = "lspSettings", storages = [Storage("aws.xml", roamingType = RoamingType.DISABLED)])
Expand All @@ -26,11 +26,7 @@
fun getArtifactPath() = state.artifactPath

fun setArtifactPath(artifactPath: String?) {
if (artifactPath == null) {
state.artifactPath = ""
} else {
state.artifactPath = artifactPath
}
state.artifactPath = artifactPath.nullize(nullizeSpaces = true)

Check warning on line 29 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt#L29

Added line #L29 was not covered by tests
}

companion object {
Expand All @@ -39,6 +35,5 @@
}

class LspConfiguration : BaseState() {
@get:Property
var artifactPath: String = ""
var artifactPath by string()

Check warning on line 38 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt

View check run for this annotation

Codecov / codecov/patch

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/settings/LspSettings.kt#L38

Added line #L38 was not covered by tests
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

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

import com.intellij.testFramework.utils.io.createDirectory
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assumptions.assumeTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import software.aws.toolkits.core.utils.ZIP_PROPERTY_POSIX
import software.aws.toolkits.core.utils.hasPosixFilePermissions
import software.aws.toolkits.core.utils.putNextEntry
import software.aws.toolkits.core.utils.test.assertPosixPermissions
import software.aws.toolkits.core.utils.writeText
import software.aws.toolkits.jetbrains.utils.satisfiesKt
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.attribute.PosixFilePermissions
import java.util.zip.ZipOutputStream
import kotlin.io.path.isRegularFile
import kotlin.io.path.setPosixFilePermissions

class LspUtilsTest {
@Test
fun `extractZipFile works`(@TempDir tempDir: Path) {
val source = tempDir.resolve("source").also { it.createDirectory() }
val target = tempDir.resolve("target").also { it.createDirectory() }

source.resolve("file1").writeText("contents1")
source.resolve("file2").writeText("contents2")
source.resolve("file3").writeText("contents3")

val sourceZip = tempDir.resolve("source.zip")
ZipOutputStream(Files.newOutputStream(sourceZip)).use { zip ->
Files.walk(source).use { paths ->
paths
.filter { it.isRegularFile() }
.forEach {
zip.putNextEntry(source.relativize(it).toString(), it)
}
val precedingSlashFile = source.resolve("file4").also { it.writeText("contents4") }
zip.putNextEntry("/${source.relativize(precedingSlashFile)}", precedingSlashFile)
}
}

extractZipFile(sourceZip, target)

assertThat(target).satisfiesKt {
val files = Files.list(it).use { stream -> stream.toList() }
assertThat(files.size).isEqualTo(4)
assertThat(target.resolve("file1")).hasContent("contents1")
assertThat(target.resolve("file2")).hasContent("contents2")
assertThat(target.resolve("file3")).hasContent("contents3")
assertThat(target.resolve("file4")).hasContent("contents4")
}
}

@Test
fun `extractZipFile respects posix`(@TempDir tempDir: Path) {
assumeTrue(tempDir.hasPosixFilePermissions())

val source = tempDir.resolve("source").also { it.createDirectory() }
val target = tempDir.resolve("target").also { it.createDirectory() }

source.resolve("regularFile").also {
it.writeText("contents1")
it.setPosixFilePermissions(PosixFilePermissions.fromString("rw-r--r--"))
}
source.resolve("executableFile").also {
it.writeText("contents2")
it.setPosixFilePermissions(PosixFilePermissions.fromString("rwxr-xr-x"))
}

val sourceZip = tempDir.resolve("source.zip")
FileSystems.newFileSystem(
sourceZip,
mapOf(
"create" to true,
ZIP_PROPERTY_POSIX to true,
)
).use { zipfs ->
Files.walk(source).use { paths ->
paths
.filter { it.isRegularFile() }
.forEach { file ->
Files.copy(file, zipfs.getPath("/").resolve(source.relativize(file).toString()), StandardCopyOption.COPY_ATTRIBUTES)
}
}
}

extractZipFile(sourceZip, target)

assertThat(target).satisfiesKt {
val files = Files.list(it).use { stream -> stream.toList() }
assertThat(files.size).isEqualTo(2)
assertPosixPermissions(target.resolve("regularFile"), "rw-r--r--")
assertPosixPermissions(target.resolve("executableFile"), "rwxr-xr-x")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,35 @@ class LspSettingsTest {
}

@Test
fun `artifact path is empty by default`() {
assertThat(lspSettings.getArtifactPath()).isEmpty()
fun `artifact path is null by default`() {
assertThat(lspSettings.getArtifactPath()).isNull()
}

@Test
fun `artifact path can be set`() {
lspSettings.setArtifactPath("test\\lsp.js")
assertThat(lspSettings.getArtifactPath()).isNotEmpty()
assertThat(lspSettings.getArtifactPath()).isEqualTo("test\\lsp.js")
assertThat(lspSettings.getArtifactPath())
.isEqualTo("test\\lsp.js")
}

@Test
fun `artifact path cannot be null`() {
lspSettings.setArtifactPath(null)
assertThat(lspSettings.getArtifactPath()).isEmpty()
fun `empty artifact path is null`() {
lspSettings.setArtifactPath("")
assertThat(lspSettings.getArtifactPath()).isNull()
}

@Test
fun `blank artifact path is null`() {
lspSettings.setArtifactPath(" ")
assertThat(lspSettings.getArtifactPath()).isNull()
}

@Test
fun `serialize settings to ensure backwards compatibility`() {
val element = xmlElement(
"""
<component name="LspSettings">
</component>
</component>
""".trimIndent()
)
lspSettings.setArtifactPath("temp\\lsp.js")
Expand All @@ -51,22 +57,26 @@ class LspSettingsTest {

val actual = XMLOutputter().outputString(element)

val expected = "<component name=\"LspSettings\">\n" +
"<option name=\"artifactPath\" value=\"temp\\lsp.js\" /></component>"
// language=XML
val expected = """
<component name="LspSettings">
<option name="artifactPath" value="temp\lsp.js" />
</component>
""".trimIndent()

assertThat(actual).isEqualTo(expected)
assertThat(actual).isEqualToIgnoringWhitespace(expected)
}

@Test
fun `deserialize empty settings to ensure backwards compatibility`() {
val element = xmlElement(
"""
<component name="LspSettings">
</component>
"""
<component name="LspSettings">
</component>
"""
)
val actual = XmlSerializer.deserialize(element, LspConfiguration::class.java)
assertThat(actual.artifactPath).isEmpty()
assertThat(actual.artifactPath).isNull()
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,3 +212,6 @@ private fun<T> tryOrLogShortException(log: Logger, block: () -> T) = try {
log.warn { "${e::class.simpleName}: ${e.message}" }
null
}

// https://github.com/corretto/corretto-21/blob/364eb35886643e504344136075f4a2442d6c0cb0/src/jdk.zipfs/share/classes/jdk/nio/zipfs/ZipFileSystem.java#L90C33-L90C78
const val ZIP_PROPERTY_POSIX = "enablePosixFileAttributes"
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

package software.aws.toolkits.core.utils

import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
Expand All @@ -17,7 +16,7 @@ import java.util.zip.ZipOutputStream
*/
fun ZipOutputStream.putNextEntry(entryName: String, file: Path) {
try {
BufferedInputStream(Files.newInputStream(file)).use { inputStream ->
Files.newInputStream(file).buffered().use { inputStream ->
putNextEntry(entryName, inputStream)
}
} catch (e: IOException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1256,7 +1256,7 @@ ecs.service.not_found=Service {0} not found in cluster {1}
ecs.task_definition.json_schema_name=AWS ECS Task Definition
ecs.task_definitions=Task Definitions
environment.variables.dialog.title=Environment Variables
executableCommon.auto_managed=Managed by AWS Toolkit
executableCommon.auto_managed=Managed by AWS
executableCommon.auto_resolved=Auto-detected: {0}
executableCommon.cli_not_configured={0} executable not configured
executableCommon.configurable.title=External Tools
Expand Down
Loading