Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(amazonq): Add support for multi-project workspaces. #5411

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -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 @@ -308,7 +308,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 +405,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 +952,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 +999,15 @@ class DocController(
return@withContext
}

if (selectedFolder.path == projectRoot.path) {
if (selectedFolder.path == workspaceRoot.toString()) {
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 @@ -249,7 +249,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 +336,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 +729,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.toString())

if (hasDevFile && !isPromptedForAutoBuildFeature) {
promptAllowQCommandsConsent(messenger, tabId)
Expand Down Expand Up @@ -812,8 +812,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 +840,7 @@ class FeatureDevController(
}

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

messenger.sendAnswer(
Expand All @@ -860,7 +860,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 @@ -49,7 +49,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.toString())
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
Loading