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 complex workspaces for /dev and /doc. #5411

Merged
merged 1 commit into from
Mar 12, 2025
Merged
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
@@ -0,0 +1,4 @@
{
"type" : "feature",
"description" : "AmazonQ /dev and /doc: Add support for complex workspaces."
}
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 @@ -13,9 +13,6 @@ import com.intellij.diff.util.DiffUserDataKeys
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.application.runInEdt
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.openapi.vfs.LocalFileSystem
import com.intellij.openapi.vfs.VfsUtil
import com.intellij.openapi.wm.ToolWindowManager
import com.intellij.testFramework.LightVirtualFile
Expand All @@ -31,9 +28,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 @@ -81,7 +78,6 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.Cancellat
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.QFeatureEvent
import software.aws.toolkits.jetbrains.services.codewhisperer.telemetry.broadcastQEvent
import software.aws.toolkits.resources.message
import java.nio.file.Paths
import java.util.UUID

enum class DocGenerationStep {
Expand Down Expand Up @@ -308,7 +304,7 @@ class DocController(
val session = getSessionInfo(tabId)
val docGenerationTask = docGenerationTasks.getTask(tabId)

val currentSourceFolder = session.context.selectedSourceFolder
val currentSourceFolder = session.context.selectionRoot

try {
messenger.sendFolderConfirmationMessage(
Expand Down Expand Up @@ -405,7 +401,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.addressableRoot)
val leftDiffContent = if (existingFile == null) {
EmptyContent()
} else {
Expand Down Expand Up @@ -945,19 +941,9 @@ class DocController(
}
}

private fun isFolderPathInProjectModules(project: Project, folderPath: String): Boolean {
val path = Paths.get(folderPath)
val virtualFile = LocalFileSystem.getInstance().findFileByIoFile(path.toFile()) ?: return false

val projectFileIndex = ProjectRootManager.getInstance(project).fileIndex

return projectFileIndex.isInProject(virtualFile)
}

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

withContext(EDT) {
Expand All @@ -967,7 +953,7 @@ class DocController(
message = message("amazonqDoc.prompt.choose_folder_to_continue")
)

val selectedFolder = selectFolder(context.project, currentSourceFolder)
val selectedFolder = selectFolder(context.project, workspaceRoot)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should it be session.context.selectionRoot instead ?

Copy link
Contributor Author

@ctidd ctidd Mar 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a small change in behavior, which handles a niche case slightly more in line with a workspace-oriented view. This behavior changes when a user selects a folder, and then triggers folder selection again.

  • Before: The file tree is opened on the current source folder to select new source folder.
  • Now: The file tree is opened on the workspace root to select a new source folder. (Like it was for the initial selection.)

This is a more workspace-oriented behavior (i.e. "select what you want") instead of nudging the user toward a folder inside their selected working folder, when the user selects a different folder multiple times.


// No folder was selected
if (selectedFolder == null) {
Expand All @@ -980,9 +966,7 @@ class DocController(
return@withContext
}

val isFolderPathInProject = isFolderPathInProjectModules(context.project, selectedFolder.path)

if (!isFolderPathInProject) {
if (!selectedFolder.path.startsWith(workspaceRoot.path)) {
logger.info { "Selected folder not in workspace: ${selectedFolder.path}" }

messenger.sendAnswer(
Expand All @@ -1004,15 +988,15 @@ class DocController(
return@withContext
}

if (selectedFolder.path == projectRoot.path) {
if (selectedFolder.path == workspaceRoot.path) {
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.selectionRoot = selectedFolder

promptForDocTarget(tabId)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,14 +107,12 @@ 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()
filePaths.forEach { resolveAndCreateOrUpdateFile(context.addressableRoot.toNioPath(), it.zipFilePath, it.fileContent) }

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

deletedFiles.forEach { resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath) }
deletedFiles.forEach { resolveAndDeleteFile(context.addressableRoot.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.addressableRoot)
}

private fun getFromReportedChanges(filePath: NewFileZipInfo): String? {
Expand Down Expand Up @@ -158,7 +156,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.addressableRoot)?.content()
.orEmpty()
val diffMetrics = getDiffMetrics(sourceContent, content)
totalAddedLines += diffMetrics.insertedLines
Expand All @@ -185,7 +183,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.addressableRoot)?.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 @@ -4,30 +4,6 @@
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.amazonqDoc.SUPPORTED_DIAGRAM_EXT_SET
import software.aws.toolkits.jetbrains.services.amazonqDoc.SUPPORTED_DIAGRAM_FILE_NAME_SET
import software.aws.toolkits.jetbrains.services.amazonq.project.FeatureDevSessionContext

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)
}
}
class DocSessionContext(project: Project, maxProjectSizeBytes: Long? = null) : FeatureDevSessionContext(project, maxProjectSizeBytes)
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.addressableRoot)

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.addressableRoot)
// 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.path)

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

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

val modifyFolderFollowUp = FollowUp(
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
Expand All @@ -825,7 +824,7 @@ class FeatureDevController(
var reason: ModifySourceFolderErrorReason? = null

withContext(EDT) {
val selectedFolder = selectFolder(context.project, currentSourceFolder)
val selectedFolder = selectFolder(context.project, workspaceRoot)
// No folder was selected
if (selectedFolder == null) {
logger.info { "Cancelled dialog and not selected any folder" }
Expand All @@ -839,8 +838,7 @@ class FeatureDevController(
return@withContext
}

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

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

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

session.context.selectedSourceFolder = selectedFolder
session.context.selectionRoot = 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.addressableRoot
.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.path)
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,14 +130,13 @@ 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()

runCatching {
var insertedLines = 0
var insertedCharacters = 0
filePaths.forEach { file ->
// FIXME: Ideally, the before content should be read from the uploaded context instead of from disk, to avoid drift
val before = selectedSourceFolder
val before = context.addressableRoot.toNioPath()
.resolve(file.zipFilePath)
.toFile()
.let { f ->
Expand Down Expand Up @@ -174,18 +173,16 @@ 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.addressableRoot)
}

// 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()

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

deletedFiles.forEach {
resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath)
resolveAndDeleteFile(context.addressableRoot.toNioPath(), it.zipFilePath)
it.changeApplied = true
}
}
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
Loading
Loading