Skip to content

Commit

Permalink
feat(amazonq): Add support for multi-project workspaces.
Browse files Browse the repository at this point in the history
  • Loading branch information
C Tidd committed Feb 26, 2025
1 parent e6f3806 commit a7c114d
Show file tree
Hide file tree
Showing 19 changed files with 260 additions and 187 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package software.aws.toolkits.jetbrains.common.session

import software.aws.toolkits.jetbrains.common.util.AmazonQCodeGenService
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.project.FeatureDevSessionContext

open class SessionStateConfig(
open val conversationId: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,9 @@ import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.common.util.selectFolder
import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.project.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
import software.aws.toolkits.jetbrains.services.amazonqDoc.DEFAULT_RETRY_LIMIT
import software.aws.toolkits.jetbrains.services.amazonqDoc.DIAGRAM_SVG_EXT
Expand Down Expand Up @@ -83,6 +83,7 @@ import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcas
import software.aws.toolkits.resources.message
import java.nio.file.Paths
import java.util.UUID
import kotlin.io.path.pathString

enum class DocGenerationStep {
UPLOAD_TO_S3,
Expand Down Expand Up @@ -308,7 +309,7 @@ class DocController(
private suspend fun promptForDocTarget(tabId: String) {
val session = getSessionInfo(tabId)

val currentSourceFolder = session.context.selectedSourceFolder
val currentSourceFolder = session.context.selectedRoot

try {
messenger.sendFolderConfirmationMessage(
Expand Down Expand Up @@ -405,7 +406,7 @@ class DocController(
inMemoryFile.isWritable = false
FileEditorManager.getInstance(context.project).openFile(inMemoryFile, true)
} else {
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedRoot)
val leftDiffContent = if (existingFile == null) {
EmptyContent()
} else {
Expand Down Expand Up @@ -952,8 +953,8 @@ class DocController(

private suspend fun modifyDefaultSourceFolder(tabId: String) {
val session = getSessionInfo(tabId)
val currentSourceFolder = session.context.selectedSourceFolder
val projectRoot = session.context.projectRoot
val currentSourceFolder = session.context.selectedRoot
val workspaceRoot = session.context.workspaceRoot

withContext(EDT) {
messenger.sendAnswer(
Expand Down Expand Up @@ -999,15 +1000,15 @@ class DocController(
return@withContext
}

if (selectedFolder.path == projectRoot.path) {
if (selectedFolder.path == workspaceRoot.pathString) {
docGenerationTask.folderLevel = DocFolderLevel.ENTIRE_WORKSPACE
} else {
docGenerationTask.folderLevel = DocFolderLevel.SUB_FOLDER
}

logger.info { "Selected correct folder inside workspace: ${selectedFolder.path}" }

session.context.selectedSourceFolder = selectedFolder
session.context.selectedRoot = selectedFolder

promptForDocTarget(tabId)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,14 @@ class DocSession(val tabID: String, val project: Project) {
* Triggered by the Insert code follow-up button to apply code changes.
*/
fun insertChanges(filePaths: List<NewFileZipInfo>, deletedFiles: List<DeletedFileInfo>) {
val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
val selectedSourceFolder = context.selectedRoot

filePaths.forEach { resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent) }
filePaths.forEach { resolveAndCreateOrUpdateFile(selectedSourceFolder.toNioPath(), it.zipFilePath, it.fileContent) }

deletedFiles.forEach { resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath) }
deletedFiles.forEach { resolveAndDeleteFile(selectedSourceFolder.toNioPath(), it.zipFilePath) }

// Taken from https://intellij-support.jetbrains.com/hc/en-us/community/posts/206118439-Refresh-after-external-changes-to-project-structure-and-sources
VfsUtil.markDirtyAndRefresh(true, true, true, context.selectedSourceFolder)
VfsUtil.markDirtyAndRefresh(true, true, true, context.selectedRoot)
}

private fun getFromReportedChanges(filePath: NewFileZipInfo): String? {
Expand Down Expand Up @@ -158,7 +158,7 @@ class DocSession(val tabID: String, val project: Project) {
}
} else {
val sourceContent = reportedChange
?: VfsUtil.findRelativeFile(filePath.zipFilePath, context.selectedSourceFolder)?.content()
?: VfsUtil.findRelativeFile(filePath.zipFilePath, context.selectedRoot)?.content()
.orEmpty()
val diffMetrics = getDiffMetrics(sourceContent, content)
totalAddedLines += diffMetrics.insertedLines
Expand All @@ -185,7 +185,7 @@ class DocSession(val tabID: String, val project: Project) {
totalAddedChars += content.length
totalAddedLines += content.split('\n').size
} else {
val existingFileContent = VfsUtil.findRelativeFile(filePath.zipFilePath, context.selectedSourceFolder)?.content()
val existingFileContent = VfsUtil.findRelativeFile(filePath.zipFilePath, context.selectedRoot)?.content()
val diffMetrics = getDiffMetrics(existingFileContent.orEmpty(), content)
totalAddedLines += diffMetrics.insertedLines
totalAddedChars += diffMetrics.insertedCharacters
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,11 @@ package software.aws.toolkits.jetbrains.services.amazonqDoc.session

import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.project.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonqDoc.SUPPORTED_DIAGRAM_EXT_SET
import software.aws.toolkits.jetbrains.services.amazonqDoc.SUPPORTED_DIAGRAM_FILE_NAME_SET

class DocSessionContext(project: Project, maxProjectSizeBytes: Long? = null) : FeatureDevSessionContext(project, maxProjectSizeBytes) {

/**
* Ensure diagram files are not ignored
*/
override fun getAdditionalGitIgnoreBinaryFilesRules(): Set<String> {
val ignoreRules = super.getAdditionalGitIgnoreBinaryFilesRules()
val diagramExtRulesInGitIgnoreFormatSet = SUPPORTED_DIAGRAM_EXT_SET.map { "*.$it" }.toSet()
return ignoreRules - diagramExtRulesInGitIgnoreFormatSet
}

/**
* Ensure diagram files are not filtered
*/
override fun isFileExtensionAllowed(file: VirtualFile): Boolean {
if (super.isFileExtensionAllowed(file)) {
return true
}

return file.extension != null && SUPPORTED_DIAGRAM_FILE_NAME_SET.contains(file.name)
}
override fun shouldIncludeFileIfNoExplicitIgnore(file: VirtualFile): Boolean =
SUPPORTED_DIAGRAM_EXT_SET.any { file.path.endsWith(it) } || SUPPORTED_DIAGRAM_FILE_NAME_SET.contains(file.name)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

package software.aws.toolkits.jetbrains.services.amazonqFeatureDev

import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.project.RepoSizeError
import software.aws.toolkits.resources.message

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ import software.aws.toolkits.core.utils.info
import software.aws.toolkits.core.utils.warn
import software.aws.toolkits.jetbrains.common.util.selectFolder
import software.aws.toolkits.jetbrains.core.coroutines.EDT
import software.aws.toolkits.jetbrains.services.amazonq.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonq.project.RepoSizeError
import software.aws.toolkits.jetbrains.services.amazonq.toolwindow.AmazonQToolWindowFactory
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationLimitException
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
Expand Down Expand Up @@ -87,6 +87,7 @@ import software.aws.toolkits.telemetry.AmazonqTelemetry
import software.aws.toolkits.telemetry.Result
import software.aws.toolkits.telemetry.UiTelemetry
import java.util.UUID
import kotlin.io.path.pathString

class FeatureDevController(
private val context: AmazonQAppInitContext,
Expand Down Expand Up @@ -249,7 +250,7 @@ class FeatureDevController(
when (sessionState) {
is PrepareCodeGenerationState -> {
runInEdt {
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedRoot)

val leftDiffContent = if (existingFile == null) {
EmptyContent()
Expand Down Expand Up @@ -336,7 +337,7 @@ class FeatureDevController(
var pollAttempt = 0
val pollDelayMs = 10L
while (pollAttempt < 5) {
val file = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
val file = VfsUtil.findRelativeFile(message.filePath, session.context.selectedRoot)
// Wait for the file to be created and/or updated to the new content:
if (file != null && file.content() == filePaths.find { it.zipFilePath == fileToUpdate }?.fileContent) {
// Open a diff, showing the changes have been applied and the file now has identical left/right state:
Expand Down Expand Up @@ -729,7 +730,7 @@ class FeatureDevController(

val codeWhispererSettings = CodeWhispererSettings.getInstance().getAutoBuildSetting()
val hasDevFile = session.context.checkForDevFile()
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.getWorkspaceRoot())
val isPromptedForAutoBuildFeature = codeWhispererSettings.containsKey(session.context.workspaceRoot.pathString)

if (hasDevFile && !isPromptedForAutoBuildFeature) {
promptAllowQCommandsConsent(messenger, tabId)
Expand Down Expand Up @@ -812,8 +813,8 @@ class FeatureDevController(

private suspend fun modifyDefaultSourceFolder(tabId: String) {
val session = getSessionInfo(tabId)
val currentSourceFolder = session.context.selectedSourceFolder
val projectRoot = session.context.projectRoot
val currentSourceFolder = session.context.selectedRoot
val workspaceRoot = session.context.workspaceRoot

val modifyFolderFollowUp = FollowUp(
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
Expand All @@ -840,7 +841,7 @@ class FeatureDevController(
}

// The folder is not in the workspace
if (!selectedFolder.path.startsWith(projectRoot.path)) {
if (!selectedFolder.path.startsWith(workspaceRoot.pathString)) {
logger.info { "Selected folder not in workspace: ${selectedFolder.path}" }

messenger.sendAnswer(
Expand All @@ -860,7 +861,7 @@ class FeatureDevController(

logger.info { "Selected correct folder inside workspace: ${selectedFolder.path}" }

session.context.selectedSourceFolder = selectedFolder
session.context.selectedRoot = selectedFolder
result = Result.Succeeded

messenger.sendAnswer(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class CodeGenerationState(
var insertedCharacters = 0
codeGenerationResult.newFiles.forEach { file ->
// FIXME: Ideally, the before content should be read from the uploaded context instead of from disk, to avoid drift
val before = config.repoContext.selectedSourceFolder
val before = config.repoContext.selectedRoot
.toNioPath()
.resolve(file.zipFilePath)
.toFile()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import software.aws.toolkits.telemetry.AmazonqTelemetry
import software.aws.toolkits.telemetry.AmazonqUploadIntent
import software.aws.toolkits.telemetry.Result
import java.util.UUID
import kotlin.io.path.pathString

private val logger = getLogger<PrepareCodeGenerationState>()

Expand Down Expand Up @@ -49,7 +50,7 @@ class PrepareCodeGenerationState(
messenger.sendAnswerPart(tabId = this.tabID, message = message("amazonqFeatureDev.chat_message.uploading_code"))
messenger.sendUpdatePlaceholder(tabId = this.tabID, newPlaceholder = message("amazonqFeatureDev.chat_message.uploading_code"))

val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.getWorkspaceRoot())
val isAutoBuildFeatureEnabled = CodeWhispererSettings.getInstance().isAutoBuildFeatureEnabled(this.config.repoContext.workspaceRoot.pathString)
val repoZipResult = config.repoContext.getProjectZip(isAutoBuildFeatureEnabled = isAutoBuildFeatureEnabled)
val zipFileChecksum = repoZipResult.checksum
zipFileLength = repoZipResult.contentLength
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VfsUtil
import software.aws.toolkits.jetbrains.common.util.resolveAndCreateOrUpdateFile
import software.aws.toolkits.jetbrains.common.util.resolveAndDeleteFile
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
import software.aws.toolkits.jetbrains.services.amazonq.project.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CODE_GENERATION_RETRY_LIMIT
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ConversationIdNotFoundException
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
Expand Down Expand Up @@ -130,7 +130,7 @@ class Session(val tabID: String, val project: Project) {
) {
val newFilePaths = filePaths.filter { !it.rejected && !it.changeApplied }
val newDeletedFiles = deletedFiles.filter { !it.rejected && !it.changeApplied }
val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
val selectedSourceFolder = context.selectedRoot.toNioPath()

runCatching {
var insertedLines = 0
Expand Down Expand Up @@ -174,15 +174,15 @@ class Session(val tabID: String, val project: Project) {
ReferenceLogController.addReferenceLog(references, project)

// Taken from https://intellij-support.jetbrains.com/hc/en-us/community/posts/206118439-Refresh-after-external-changes-to-project-structure-and-sources
VfsUtil.markDirtyAndRefresh(true, true, true, context.selectedSourceFolder)
VfsUtil.markDirtyAndRefresh(true, true, true, context.selectedRoot)
}

// Suppressing because insertNewFiles needs to be a suspend function in order to be tested
@Suppress("RedundantSuspendModifier")
suspend fun insertNewFiles(
filePaths: List<NewFileZipInfo>,
) {
val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
val selectedSourceFolder = context.selectedRoot.toNioPath()

filePaths.forEach {
resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent)
Expand All @@ -195,7 +195,7 @@ class Session(val tabID: String, val project: Project) {
suspend fun applyDeleteFiles(
deletedFiles: List<DeletedFileInfo>,
) {
val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
val selectedSourceFolder = context.selectedRoot.toNioPath()

deletedFiles.forEach {
resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session

import com.fasterxml.jackson.annotation.JsonValue
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.project.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.CancellationTokenSource
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
import software.aws.toolkits.jetbrains.services.cwc.messages.RecommendationContentSpan
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
// Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

import com.intellij.openapi.vfs.VirtualFile
import com.intellij.testFramework.RuleChain
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever
import software.aws.toolkits.jetbrains.services.amazonq.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonq.project.FeatureDevSessionContext
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevTestBase
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FeatureDevService
import software.aws.toolkits.jetbrains.utils.rules.HeavyJavaCodeInsightTestFixtureRule
Expand All @@ -38,39 +35,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest
featureDevSessionContext = FeatureDevSessionContext(featureDevService.project, 1024)
}

@Test
fun testWithDirectory() {
val directory = mock<VirtualFile>()
whenever(directory.extension).thenReturn(null)
whenever(directory.isDirectory).thenReturn(true)
assertTrue(featureDevSessionContext.isFileExtensionAllowed(directory))
}

@Test
fun testWithValidFile() {
val ktFile = mock<VirtualFile>()
whenever(ktFile.extension).thenReturn("kt")
whenever(ktFile.path).thenReturn("code.kt")
assertTrue(featureDevSessionContext.isFileExtensionAllowed(ktFile))
}

@Test
fun testWithInvalidFile() {
val mediaFile = mock<VirtualFile>()
whenever(mediaFile.extension).thenReturn("mp4")
assertFalse(featureDevSessionContext.isFileExtensionAllowed(mediaFile))
}

@Test
fun testAllowedFilePath() {
val allowedPaths = listOf("build.gradle", "gradle.properties", ".mvn/wrapper/maven-wrapper.properties")
allowedPaths.forEach({
val txtFile = mock<VirtualFile>()
whenever(txtFile.path).thenReturn(it)
whenever(txtFile.extension).thenReturn(it.split(".").last())
assertTrue(featureDevSessionContext.isFileExtensionAllowed(txtFile))
})
}
// FIXME: Add deeper tests, replacing previous shallow tests - BLOCKING

@Test
fun testZipProjectWithoutAutoDev() {
Expand Down Expand Up @@ -182,7 +147,7 @@ class FeatureDevSessionContextTest : FeatureDevTestBase(HeavyJavaCodeInsightTest
"src/file.png/"
)

val patterns = sampleGitIgnorePatterns.map { pattern -> featureDevSessionContext.convertGitIgnorePatternToRegex(pattern).toRegex() }
val patterns = sampleGitIgnorePatterns.map { pattern -> featureDevSessionContext.convertGitIgnorePatternToRegex(pattern) }

val matchedFiles = sampleFileNames.filter { fileName ->
patterns.any { pattern ->
Expand Down
Loading

0 comments on commit a7c114d

Please sign in to comment.