Skip to content

Commit 2054699

Browse files
committed
fix(/dev): source folder modification now allows any sub-folder
1 parent 762822c commit 2054699

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

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)