Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,21 @@ class CodeWhispererConfigurable(private val project: Project) :
.resizableColumn()
.align(Align.FILL)
}
row(message("amazonqFeatureDev.placeholder.node_runtime_path")) {
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFileDescriptor()
fileChooserDescriptor.isForcedToUseIdeaFileChooser = true

textFieldWithBrowseButton(fileChooserDescriptor = fileChooserDescriptor)
.bindText(
{ LspSettings.getInstance().getNodeRuntimePath().orEmpty() },
{ LspSettings.getInstance().setNodeRuntimePath(it) }
)
.applyToComponent {
emptyText.text = message("executableCommon.auto_managed")
}
.resizableColumn()
.align(Align.FILL)
}
}

group(message("aws.settings.codewhisperer.group.general")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@

import com.google.gson.ToNumberPolicy
import com.intellij.execution.configurations.GeneralCommandLine
import com.intellij.execution.configurations.PathEnvironmentVariableUtil
import com.intellij.execution.impl.ExecutionManagerImpl
import com.intellij.execution.process.KillableColoredProcessHandler
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessListener
import com.intellij.execution.process.ProcessOutputType
import com.intellij.notification.NotificationAction
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
import com.intellij.openapi.components.service
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.options.ShowSettingsUtil
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.Key
Expand Down Expand Up @@ -76,6 +79,8 @@
import software.aws.toolkits.jetbrains.services.amazonq.profile.QDefaultServiceConfig
import software.aws.toolkits.jetbrains.services.telemetry.ClientMetadata
import software.aws.toolkits.jetbrains.settings.LspSettings
import software.aws.toolkits.jetbrains.utils.notifyInfo
import software.aws.toolkits.resources.message
import java.io.IOException
import java.io.OutputStreamWriter
import java.io.PipedInputStream
Expand All @@ -86,7 +91,9 @@
import java.net.URI
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import java.nio.file.Path
import java.util.concurrent.Future
import java.util.concurrent.TimeUnit
import kotlin.time.Duration.Companion.seconds

// https://github.com/redhat-developer/lsp4ij/blob/main/src/main/java/com/redhat/devtools/lsp4ij/server/LSPProcessListener.java
Expand Down Expand Up @@ -372,7 +379,9 @@
}

val node = if (SystemInfo.isWindows) "node.exe" else "node"
val cmd = NodeExePatcher.patch(artifact.resolve(node))
var nodePath = getNodeRuntimePath(artifact.resolve(node))

val cmd = NodeExePatcher.patch(nodePath)
.withParameters(
LspSettings.getInstance().getArtifactPath() ?: artifact.resolve("aws-lsp-codewhisperer.js").toString(),
"--stdio",
Expand Down Expand Up @@ -492,6 +501,103 @@
}
}

/**
* Resolves the path to a valid Node.js runtime in the following order of preference:
* 1. Uses the provided nodePath if it exists and is executable
* 2. Uses user-specified runtime path from LSP settings if available
* 3. Uses system Node.js if version 18+ is available
* 4. Falls back to original nodePath with a notification to configure runtime
*
* @param nodePath The initial Node.js runtime path to check, typically from the artifact directory
* @return Path The resolved Node.js runtime path to use for the LSP server
*
* Side effects:
* - Logs warnings if initial runtime path is invalid
* - Logs info when using alternative runtime path
* - Shows notification to user if no valid Node.js runtime is found
*
* Note: The function will return a path even if no valid runtime is found, but the LSP server
* may fail to start in that case. The caller should handle potential runtime initialization failures.
*/
private fun getNodeRuntimePath(nodePath: Path): Path {
if (Files.exists(nodePath) && Files.isExecutable(nodePath)) {
return nodePath
}
// use alternative node runtime if it is not found
LOG.warn { "Node Runtime download failed. Fallback to user specified node runtime " }
// attempt to use user provided node runtime path
val nodeRuntime = LspSettings.getInstance().getNodeRuntimePath()
if (!nodeRuntime.isNullOrEmpty()) {
LOG.info { "Using node from $nodeRuntime " }
return Path.of(nodeRuntime)
} else {
val localNode = locateNodeCommand()
if (localNode != null) {
LOG.info { "Using node from ${localNode.toAbsolutePath()}" }
return localNode
}
notifyInfo(
"Amazon Q",
message("amazonqFeatureDev.placeholder.node_runtime_message"),
project = project,
listOf(
NotificationAction.create(
message("codewhisperer.actions.open_settings.title")

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

View workflow job for this annotation

GitHub Actions / qodana

Incorrect string capitalization

String 'Open Settings' is not properly capitalized. It should have sentence capitalization

Check warning

Code scanning / QDJVMC

Incorrect string capitalization Warning

String 'Open Settings' is not properly capitalized. It should have sentence capitalization
) { _, notification ->
ShowSettingsUtil.getInstance().showSettingsDialog(project, message("aws.settings.codewhisperer.configurable.title"))
},
NotificationAction.create(
message("codewhisperer.notification.custom.simple.button.got_it")
) { _, notification -> notification.expire() }
)
)
return nodePath
}
}

/**
* Locates node executable ≥18 in system PATH.
* Uses IntelliJ's PathEnvironmentVariableUtil to find executables.
*
* @return Path? The absolute path to node ≥18 if found, null otherwise
*/
private fun locateNodeCommand(): Path? {
val exeName = if (SystemInfo.isWindows) "node.exe" else "node"

return PathEnvironmentVariableUtil.findAllExeFilesInPath(exeName)
.asSequence()
.map { it.toPath() }
.filter { Files.isRegularFile(it) && Files.isExecutable(it) }
.firstNotNullOfOrNull { path ->
try {
val process = ProcessBuilder(path.toString(), "--version")
.redirectErrorStream(true)
.start()

if (!process.waitFor(2, TimeUnit.SECONDS)) {
process.destroy()
null
} else if (process.exitValue() == 0) {
val version = process.inputStream.bufferedReader().readText().trim()
val majorVersion = version.removePrefix("v").split(".")[0].toIntOrNull()

if (majorVersion != null && majorVersion >= 18) {
path.toAbsolutePath()
} else {
LOG.debug { "Node version < 18 found at: $path (version: $version)" }
null
}
} else {
LOG.debug { "Failed to get version from node at: $path" }
null
}
} catch (e: Exception) {
LOG.debug(e) { "Failed to check version for node at: $path" }
null
}
}
}

override fun dispose() {
if (!launcherFuture.isDone) {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,22 @@ class LspSettings : PersistentStateComponent<LspConfiguration> {

fun getArtifactPath() = state.artifactPath

fun getNodeRuntimePath() = state.nodeRuntimePath

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

fun setNodeRuntimePath(nodeRuntimePath: String?) {
state.nodeRuntimePath = nodeRuntimePath.nullize(nullizeSpaces = true)
}

companion object {
fun getInstance(): LspSettings = service()
}
}

class LspConfiguration : BaseState() {
var artifactPath by string()
var nodeRuntimePath by string()
}
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,8 @@ amazonqFeatureDev.placeholder.downloading_and_extracting_lsp_artifacts=Downloadi
amazonqFeatureDev.placeholder.generating_code=Generating code...
amazonqFeatureDev.placeholder.lsp=LSP
amazonqFeatureDev.placeholder.new_plan=Describe your task or issue in as much detail as possible
amazonqFeatureDev.placeholder.node_runtime_message=Please provide the absolute path of your node js v18+ runtime executable in Settings. Re-open IDE to apply this change.
amazonqFeatureDev.placeholder.node_runtime_path=Node Runtime Path
amazonqFeatureDev.placeholder.provide_code_feedback=Provide feedback or comments
amazonqFeatureDev.placeholder.select_lsp_artifact=Select LSP Artifact
amazonqFeatureDev.placeholder.write_new_prompt=Write a new prompt
Expand Down
Loading