Skip to content

Commit 9322415

Browse files
committed
fix(/dev): source folder modification now allows any sub-folder
1 parent 4a7e8f1 commit 9322415

File tree

7 files changed

+163
-25
lines changed

7 files changed

+163
-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
@@ -62,6 +61,7 @@ import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.Sessio
6261
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.session.SessionStatePhase
6362
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.storage.ChatSessionStorage
6463
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.getFollowUpOptions
64+
import software.aws.toolkits.jetbrains.services.amazonqFeatureDev.util.selectFolder
6565
import software.aws.toolkits.jetbrains.services.cwc.controller.chat.telemetry.getStartUrl
6666
import software.aws.toolkits.jetbrains.ui.feedback.FeatureDevFeedbackDialog
6767
import software.aws.toolkits.resources.message
@@ -193,7 +193,7 @@ class FeatureDevController(
193193
when (sessionState) {
194194
is PrepareCodeGenerationState -> {
195195
runInEdt {
196-
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.projectRoot)
196+
val existingFile = VfsUtil.findRelativeFile(message.filePath, session.context.currentRoot)
197197

198198
val leftDiffContent = if (existingFile == null) {
199199
EmptyContent()
@@ -605,8 +605,8 @@ class FeatureDevController(
605605

606606
private suspend fun modifyDefaultSourceFolder(tabId: String) {
607607
val session = getSessionInfo(tabId)
608-
val uri = session.context.projectRoot
609-
val fileChooserDescriptor = FileChooserDescriptorFactory.createSingleFolderDescriptor()
608+
val currentRoot = session.context.currentRoot
609+
val projectRoot = session.context.projectRoot
610610

611611
val modifyFolderFollowUp = FollowUp(
612612
pillText = message("amazonqFeatureDev.follow_up.modify_source_folder"),
@@ -615,11 +615,10 @@ class FeatureDevController(
615615
)
616616

617617
var result: Result = Result.Failed
618-
var reason: String? = null
618+
var reason: ModifySourceFolderReason? = null
619619

620620
withContext(EDT) {
621-
val selectedFolder = FileChooser.chooseFile(fileChooserDescriptor, context.project, uri)
622-
621+
val selectedFolder = selectFolder(context.project, currentRoot)
623622
// No folder was selected
624623
if (selectedFolder == null) {
625624
logger.info { "Cancelled dialog and not selected any folder" }
@@ -629,12 +628,12 @@ class FeatureDevController(
629628
followUp = listOf(modifyFolderFollowUp),
630629
)
631630

632-
reason = "ClosedBeforeSelection"
631+
reason = ModifySourceFolderReason.ClosedBeforeSelection
633632
return@withContext
634633
}
635634

636635
// The folder is not in the workspace
637-
if (selectedFolder.parent.path != uri.path) {
636+
if (!selectedFolder.path.startsWith(projectRoot.path)) {
638637
logger.info { "Selected folder not in workspace: ${selectedFolder.path}" }
639638

640639
messenger.sendAnswer(
@@ -648,13 +647,13 @@ class FeatureDevController(
648647
followUp = listOf(modifyFolderFollowUp),
649648
)
650649

651-
reason = "NotInWorkspaceFolder"
650+
reason = ModifySourceFolderReason.NotInWorkspaceFolder
652651
return@withContext
653652
}
654653

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

657-
session.context.projectRoot = selectedFolder
656+
session.context.currentRoot = selectedFolder
658657
result = Result.Succeeded
659658

660659
messenger.sendAnswer(
@@ -668,7 +667,7 @@ class FeatureDevController(
668667
amazonqConversationId = session.conversationId,
669668
credentialStartUrl = getStartUrl(project = context.project),
670669
result = result,
671-
reason = reason
670+
reason = reason?.toString()
672671
)
673672
}
674673

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 currentRootPath = context.currentRoot.toNioPath()
101101

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

104-
deletedFiles.forEach { resolveAndDeleteFile(projectRootPath, it.zipFilePath) }
104+
deletedFiles.forEach { resolveAndDeleteFile(currentRootPath, 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.currentRoot)
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.currentRoot = mock()
95+
whenever(session.context.currentRoot.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: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,9 +66,10 @@ 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+
val projectRoot = project.guessProjectDir() ?: error("Cannot guess base directory for project ${project.name}")
70+
private var _currentRoot = projectRoot
7071
private var ignorePatternsWithGitIgnore = emptyList<Regex>()
71-
private val gitIgnoreFile = File(projectRoot.path, ".gitignore")
72+
private val gitIgnoreFile = File(currentRoot.path, ".gitignore")
7273

7374
init {
7475
ignorePatternsWithGitIgnore = (ignorePatterns + parseGitIgnore().map { Regex(it) }).toList()
@@ -77,7 +78,7 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
7778
fun getProjectZip(): ZipCreationResult {
7879
val zippedProject = runBlocking {
7980
withBackgroundProgress(project, message("amazonqFeatureDev.create_plan.background_progress_title")) {
80-
zipFiles(projectRoot)
81+
zipFiles(currentRoot)
8182
}
8283
}
8384
val checkSum256: String = Base64.getEncoder().encodeToString(DigestUtils.sha256(FileInputStream(zippedProject)))
@@ -162,11 +163,11 @@ class FeatureDevSessionContext(val project: Project, val maxProjectSizeBytes: Lo
162163
.replace("*", ".*")
163164
.let { if (it.endsWith("/")) "$it?" else it } // Handle directory-specific patterns by optionally matching trailing slash
164165

165-
var projectRoot: VirtualFile
166+
var currentRoot: VirtualFile
166167
set(newRoot) {
167-
_projectRoot = newRoot
168+
_currentRoot = newRoot
168169
}
169-
get() = _projectRoot
170+
get() = _currentRoot
170171
}
171172

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

0 commit comments

Comments
 (0)