Skip to content

Commit 6759455

Browse files
authored
fix(/dev): source folder modification now allows any sub-folder (#4489)
The source folder modification happens when the customers repository is too big for uploading and /dev allows the customer to select a new source folder from within their working project. Until now, this check was faulty as it would only check for one level of sub-folder from the root, now it is any level. And of course, folders outside the workspace are not allowed. Workspace being for VSCode the project's root.
1 parent 7010c57 commit 6759455

File tree

7 files changed

+134
-25
lines changed

7 files changed

+134
-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 ModifySourceFolderErrorReason(
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.ModifySourceFolderErrorReason
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: ModifySourceFolderErrorReason? = 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 = ModifySourceFolderErrorReason.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 = ModifySourceFolderErrorReason.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: 87 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
@@ -56,6 +57,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio
5657
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
5758
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
5859
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
60+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
5961
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.uploadArtifactToS3
6062
import software.aws.toolkits.resources.message
6163
import software.aws.toolkits.telemetry.AmazonqTelemetry
@@ -394,4 +396,89 @@ class FeatureDevControllerTest : FeatureDevTestBase() {
394396
newFileContentsCopy[0].rejected = !newFileContentsCopy[0].rejected
395397
coVerify { messenger.updateFileComponent(testTabId, newFileContentsCopy, deletedFiles, "") }
396398
}
399+
400+
@Test
401+
fun `test modifyDefaultSourceFolder customer does not select a folder`() = runTest {
402+
val followUp = FollowUp(FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER, pillText = "Modify default source folder")
403+
val message = IncomingFeatureDevMessage.FollowupClicked(followUp, testTabId, "", "test-command")
404+
405+
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
406+
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
407+
408+
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
409+
every { selectFolder(any(), any()) } returns null
410+
411+
spySession.preloader(userMessage, messenger)
412+
controller.processFollowupClickedMessage(message)
413+
414+
coVerifyOrder {
415+
messenger.sendSystemPrompt(
416+
tabId = testTabId,
417+
followUp = listOf(
418+
FollowUp(
419+
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
420+
type = FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER,
421+
status = FollowUpStatusType.Info,
422+
)
423+
)
424+
)
425+
}
426+
}
427+
428+
@Test
429+
fun `test modifyDefaultSourceFolder customer selects a folder outside the workspace`() = runTest {
430+
val followUp = FollowUp(FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER, pillText = "Modify default source folder")
431+
val message = IncomingFeatureDevMessage.FollowupClicked(followUp, testTabId, "", "test-command")
432+
433+
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
434+
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
435+
436+
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
437+
every { selectFolder(any(), any()) } returns LightVirtualFile("/path")
438+
439+
spySession.preloader(userMessage, messenger)
440+
controller.processFollowupClickedMessage(message)
441+
442+
coVerifyOrder {
443+
messenger.sendAnswer(
444+
tabId = testTabId,
445+
messageType = FeatureDevMessageType.Answer,
446+
message = message("amazonqFeatureDev.follow_up.incorrect_source_folder")
447+
)
448+
messenger.sendSystemPrompt(
449+
tabId = testTabId,
450+
followUp = listOf(
451+
FollowUp(
452+
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
453+
type = FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER,
454+
status = FollowUpStatusType.Info,
455+
)
456+
)
457+
)
458+
}
459+
}
460+
461+
@Test
462+
fun `test modifyDefaultSourceFolder customer selects a correct sub folder`() = runTest {
463+
val followUp = FollowUp(FollowUpTypes.MODIFY_DEFAULT_SOURCE_FOLDER, pillText = "Modify default source folder")
464+
val message = IncomingFeatureDevMessage.FollowupClicked(followUp, testTabId, "", "test-command")
465+
466+
whenever(featureDevClient.createTaskAssistConversation()).thenReturn(exampleCreateTaskAssistConversationResponse)
467+
whenever(chatSessionStorage.getSession(any(), any())).thenReturn(spySession)
468+
469+
val folder = LightVirtualFile("${spySession.context.projectRoot.name}/path/to/sub/folder")
470+
mockkStatic("software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.FileUtilsKt")
471+
every { selectFolder(any(), any()) } returns folder
472+
473+
spySession.preloader(userMessage, messenger)
474+
controller.processFollowupClickedMessage(message)
475+
476+
coVerify {
477+
messenger.sendAnswer(
478+
tabId = testTabId,
479+
messageType = FeatureDevMessageType.Answer,
480+
message = message("amazonqFeatureDev.follow_up.modified_source_folder", folder.path)
481+
)
482+
}
483+
}
397484
}

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)