Skip to content

Commit d0fc4cc

Browse files
committed
fix(/dev): source folder modification now allows any sub-folder
1 parent 8e11060 commit d0fc4cc

File tree

7 files changed

+166
-25
lines changed

7 files changed

+166
-25
lines changed

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/FeatureDevConstants.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,13 @@ const val DEFAULT_RETRY_LIMIT = 0
1616

1717
// Max allowed size for a repository in bytes
1818
const val MAX_PROJECT_SIZE_BYTES: Long = 200 * 1024 * 1024
19+
20+
enum class ModifySourceFolderReason(
21+
private val reasonText: String
22+
) {
23+
ClosedBeforeSelection("ClosedBeforeSelection"),
24+
NotInWorkspaceFolder("NotInWorkspaceFolder"),
25+
;
26+
27+
override fun toString(): String = reasonText
28+
}

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevController.kt

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import com.intellij.openapi.application.runInEdt
1313
import com.intellij.openapi.command.WriteCommandAction
1414
import com.intellij.openapi.editor.Caret
1515
import com.intellij.openapi.editor.Editor
16-
import com.intellij.openapi.fileChooser.FileChooser
17-
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
1816
import com.intellij.openapi.fileEditor.FileEditorManager
1917
import com.intellij.openapi.vfs.VfsUtil
2018
import com.intellij.openapi.wm.ToolWindowManager
@@ -33,6 +31,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.CodeIterationL
3331
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.DEFAULT_RETRY_LIMIT
3432
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FEATURE_NAME
3533
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.InboundAppMessagesHandler
34+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderReason
3635
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.MonthlyConversationLimitError
3736
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.PlanIterationLimitError
3837
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.createUserFacingErrorMessage
@@ -61,6 +60,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio
6160
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
6261
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
6362
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
63+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
6464
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
6565
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
6666
import software.aws.toolkits.resources.message
@@ -192,7 +192,7 @@ class FeatureDevController(
192192
when (sessionState) {
193193
is PrepareCodeGenerationState -> {
194194
runInEdt {
195-
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.projectRoot)
195+
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.selectedSourceFolder)
196196

197197
val leftDiffContent = if (existingFile == null) {
198198
EmptyContent()
@@ -602,8 +602,8 @@ class FeatureDevController(
602602

603603
private suspend fun modifyDefaultSourceFolder(tabId: String) {
604604
val session = getSessionInfo(tabId)
605-
val uri = session.context.projectRoot
606-
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
605+
val currentSourceFolder = session.context.selectedSourceFolder
606+
val projectRoot = session.context.projectRoot
607607

608608
val modifyFolderFollowUp = FollowUp(
609609
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
@@ -612,11 +612,10 @@ class FeatureDevController(
612612
)
613613

614614
var result: Result = Result.Failed
615-
var reason: String? = null
615+
var reason: ModifySourceFolderReason? = null
616616

617617
withContext(EDT) {
618-
val selectedFolder = FileChooser.chooseFile(fileChooserDescriptor, context.project, uri)
619-
618+
val selectedFolder = selectFolder(context.project, currentSourceFolder)
620619
// No folder was selected
621620
if (selectedFolder == null) {
622621
logger.info { "Cancelled dialog and not selected any folder" }
@@ -626,12 +625,12 @@ class FeatureDevController(
626625
followUp = listOf(modifyFolderFollowUp),
627626
)
628627

629-
reason = "ClosedBeforeSelection"
628+
reason = ModifySourceFolderReason.ClosedBeforeSelection
630629
return@withContext
631630
}
632631

633632
// The folder is not in the workspace
634-
if (selectedFolder.parent.path != uri.path) {
633+
if (!selectedFolder.path.startsWith(projectRoot.path)) {
635634
logger.info { "Selected folder not in workspace: ${selectedFolder.path}" }
636635

637636
messenger.sendAnswer(
@@ -645,13 +644,13 @@ class FeatureDevController(
645644
followUp = listOf(modifyFolderFollowUp),
646645
)
647646

648-
reason = "NotInWorkspaceFolder"
647+
reason = ModifySourceFolderReason.NotInWorkspaceFolder
649648
return@withContext
650649
}
651650

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

654-
session.context.projectRoot = selectedFolder
653+
session.context.selectedSourceFolder = selectedFolder
655654
result = Result.Succeeded
656655

657656
messenger.sendAnswer(
@@ -665,7 +664,7 @@ class FeatureDevController(
665664
amazonqConversationId = session.conversationId,
666665
credentialStartUrl = getStartUrl(project = context.project),
667666
result = result,
668-
reason = reason
667+
reason = reason?.toString()
669668
)
670669
}
671670

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/Session.kt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -97,16 +97,16 @@ class Session(val tabID: String, val project: Project) {
9797
* Triggered by the Insert code follow-up button to apply code changes.
9898
*/
9999
fun insertChanges(filePaths: List<NewFileZipInfo>, deletedFiles: List<DeletedFileInfo>, references: List<CodeReferenceGenerated>) {
100-
val projectRootPath = context.projectRoot.toNioPath()
100+
val selectedSourceFolder = context.selectedSourceFolder.toNioPath()
101101

102-
filePaths.forEach { resolveAndCreateOrUpdateFile(projectRootPath, it.zipFilePath, it.fileContent) }
102+
filePaths.forEach { resolveAndCreateOrUpdateFile(selectedSourceFolder, it.zipFilePath, it.fileContent) }
103103

104-
deletedFiles.forEach { resolveAndDeleteFile(projectRootPath, it.zipFilePath) }
104+
deletedFiles.forEach { resolveAndDeleteFile(selectedSourceFolder, it.zipFilePath) }
105105

106106
ReferenceLogController.addReferenceLog(references, project)
107107

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

112112
suspend fun send(msg: String): Interaction {

plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/util/FileUtils.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util
55

6+
import com.intellij.openapi.fileChooser.FileChooser
7+
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
8+
import com.intellij.openapi.project.Project
9+
import com.intellij.openapi.vfs.VirtualFile
610
import java.nio.file.Path
711
import kotlin.io.path.createDirectories
812
import kotlin.io.path.deleteIfExists
@@ -24,3 +28,8 @@ fun resolveAndDeleteFile(projectRootPath: Path, relativePath: String) {
2428
val filePath = projectRootPath.resolve(relativePath)
2529
filePath.deleteIfExists()
2630
}
31+
32+
fun selectFolder(project: Project, openOn: VirtualFile): VirtualFile? {
33+
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
34+
return FileChooser.chooseFile(fileChooserDescriptor, project, openOn)
35+
}

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/controller/FeatureDevControllerTest.kt

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
package software.aws.toolkits.jetbrains.services.amazonqFeatureDev.controller
55

6+
import com.intellij.testFramework.LightVirtualFile
67
import com.intellij.testFramework.RuleChain
78
import com.intellij.testFramework.replaceService
89
import io.mockk.coEvery
@@ -34,6 +35,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthController
3435
import software.aws.toolkits.jetbrains.services.amazonq.auth.AuthNeededStates
3536
import software.aws.toolkits.jetbrains.services.amazonq.messages.MessagePublisher
3637
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.FeatureDevTestBase
38+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.ModifySourceFolderReason
3739
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.clients.FeatureDevClient
3840
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FeatureDevMessageType
3941
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.messages.FollowUp
@@ -56,9 +58,11 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio
5658
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
5759
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
5860
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
61+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
5962
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
6063
import software.aws.toolkits.resources.message
6164
import software.aws.toolkits.telemetry.AmazonqTelemetry
65+
import software.aws.toolkits.telemetry.Result
6266
import org.mockito.kotlin.verify as mockitoVerify
6367

6468
class FeatureDevControllerTest : FeatureDevTestBase() {
@@ -394,4 +398,119 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
394398
newFileContentsCopy[0].rejected = !newFileContentsCopy[0].rejected
395399
coVerify { messenger.updateFileComponent(testTabId, newFileContentsCopy, deletedFiles, "") }
396400
}
401+
402+
@Test
403+
fun `test modifyDefaultSourceFolder customer does not select a folder`() = runTest {
404+
val followUp = FollowUp(FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER, pillText = "Modify default source folder")
405+
val message = IncomingFeatureDevMessage.FollowupClicked(followUp, testTabId, "", "test-command")
406+
407+
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
408+
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
409+
410+
mockkObject(AmazonqTelemetry)
411+
every { AmazonqTelemetry.modifySourceFolder(amazonqConversationId = any()) } just runs
412+
413+
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
414+
every { selectFolder(any(), any()) } returns null
415+
416+
spySession.preloader(userMessage, messenger)
417+
controller.processFollowupClickedMessage(message)
418+
419+
coVerifyOrder {
420+
messenger.sendSystemPrompt(
421+
tabId = testTabId,
422+
followUp = listOf(
423+
FollowUp(
424+
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
425+
type = FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER,
426+
status = FollowUpStatusType.Info,
427+
)
428+
)
429+
)
430+
AmazonqTelemetry.modifySourceFolder(
431+
amazonqConversationId = spySession.conversationId,
432+
credentialStartUrl = any(),
433+
result = Result.Failed,
434+
reason = ModifySourceFolderReason.ClosedBeforeSelection.toString(),
435+
createTime = any()
436+
)
437+
}
438+
}
439+
440+
@Test
441+
fun `test modifyDefaultSourceFolder customer selects a folder outside the workspace`() = runTest {
442+
val followUp = FollowUp(FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER, pillText = "Modify default source folder")
443+
val message = IncomingFeatureDevMessage.FollowupClicked(followUp, testTabId, "", "test-command")
444+
445+
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
446+
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
447+
448+
mockkObject(AmazonqTelemetry)
449+
every { AmazonqTelemetry.modifySourceFolder(amazonqConversationId = any()) } just runs
450+
451+
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
452+
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
453+
454+
spySession.preloader(userMessage, messenger)
455+
controller.processFollowupClickedMessage(message)
456+
457+
coVerifyOrder {
458+
messenger.sendAnswer(
459+
tabId = testTabId,
460+
messageType = FeatureDevMessageType.Answer,
461+
message = message("amazonqFeatureDev.follow_up.incorrect_source_folder")
462+
)
463+
messenger.sendSystemPrompt(
464+
tabId = testTabId,
465+
followUp = listOf(
466+
FollowUp(
467+
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
468+
type = FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER,
469+
status = FollowUpStatusType.Info,
470+
)
471+
)
472+
)
473+
AmazonqTelemetry.modifySourceFolder(
474+
amazonqConversationId = spySession.conversationId,
475+
credentialStartUrl = any(),
476+
result = Result.Failed,
477+
reason = ModifySourceFolderReason.NotInWorkspaceFolder.toString(),
478+
createTime = any()
479+
)
480+
}
481+
}
482+
483+
@Test
484+
fun `test modifyDefaultSourceFolder customer selects a correct sub folder`() = runTest {
485+
val followUp = FollowUp(FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER, pillText = "Modify default source folder")
486+
val message = IncomingFeatureDevMessage.FollowupClicked(followUp, testTabId, "", "test-command")
487+
488+
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
489+
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
490+
491+
mockkObject(AmazonqTelemetry)
492+
every { AmazonqTelemetry.modifySourceFolder(amazonqConversationId = any()) } just runs
493+
494+
val folder = LightVirtualFile("${spySession.context.projectRoot.name}/path/to/sub/folder")
495+
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
496+
every { selectFolder(any(), any()) } returns folder
497+
498+
spySession.preloader(userMessage, messenger)
499+
controller.processFollowupClickedMessage(message)
500+
501+
coVerify {
502+
messenger.sendAnswer(
503+
tabId = testTabId,
504+
messageType = FeatureDevMessageType.Answer,
505+
message = message("amazonqFeatureDev.follow_up.modified_source_folder", folder.path)
506+
)
507+
AmazonqTelemetry.modifySourceFolder(
508+
amazonqConversationId = spySession.conversationId,
509+
credentialStartUrl = any(),
510+
result = Result.Succeeded,
511+
reason = isNull(),
512+
createTime = any()
513+
)
514+
}
515+
}
397516
}

plugins/amazonq/chat/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonqFeatureDev/session/SessionTest.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ class SessionTest : FeatureDevTestBase() {
9191
val mockNewFile = listOf(NewFileZipInfo("test.ts", "testContent", false))
9292
val mockDeletedFile = listOf(DeletedFileInfo("deletedTest.ts", false))
9393

94-
session.context.projectRoot = mock()
95-
whenever(session.context.projectRoot.toNioPath()).thenReturn(Path(""))
94+
session.context.selectedSourceFolder = mock()
95+
whenever(session.context.selectedSourceFolder.toNioPath()).thenReturn(Path(""))
9696

9797
session.insertChanges(mockNewFile, mockDeletedFile, emptyList())
9898

plugins/core/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/FeatureDevSessionContext.kt

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,13 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
6666
"dist/"
6767
).map { Regex(it) }
6868

69-
private var _projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
69+
// projectRoot: is the directory where the project is located when selected to open a project.
70+
val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
71+
72+
// selectedSourceFolder": is the directory selected in replacement of the root, this happens when the project is too big to bundle for uploading.
73+
private var _selectedSourceFolder = projectRoot
7074
private var ignorePatternsWithGitIgnore = emptyList<Regex>()
71-
private val gitIgnoreFile = File(projectRoot.path, ".gitignore")
75+
private val gitIgnoreFile = File(selectedSourceFolder.path, ".gitignore")
7276

7377
init {
7478
ignorePatternsWithGitIgnore = (ignorePatterns + parseGitIgnore().map { Regex(it) }).toList()
@@ -77,7 +81,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
7781
fun getProjectZip(): ZipCreationResult {
7882
val zippedProject = runBlocking {
7983
withBackgroundProgress(project, message("amazonqFeatureDev.create_plan.background_progress_title")) {
80-
zipFiles(projectRoot)
84+
zipFiles(selectedSourceFolder)
8185
}
8286
}
8387
val checkSum256: String = Base64.getEncoder().encodeToString(DigestUtils.sha256(FileInputStream(zippedProject)))
@@ -162,11 +166,11 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
162166
.replace("*", ".*")
163167
.let { if (it.endsWith("/")) "$it?" else it } // Handle directory-specific patterns by optionally matching trailing slash
164168

165-
var projectRoot: VirtualFile
169+
var selectedSourceFolder: VirtualFile
166170
set(newRoot) {
167-
_projectRoot = newRoot
171+
_selectedSourceFolder = newRoot
168172
}
169-
get() = _projectRoot
173+
get() = _selectedSourceFolder
170174
}
171175

172176
data class ZipCreationResult(val payload: File, val checksum: String, val contentLength: Long)

0 commit comments

Comments
 (0)