diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index 59658c3a878..3e0e9439f93 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -317,7 +317,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs } // invokeOnCompletion results in weird lock/timeout error - initializeResult.asCompletableFuture().handleAsync { r, ex -> + initializeResult.asCompletableFuture().handleAsync { lspInitResult, ex -> if (ex != null) { return@handleAsync } @@ -325,7 +325,7 @@ private class AmazonQServerInstance(private val project: Project, private val cs this@AmazonQServerInstance.apply { DefaultAuthCredentialsService(project, encryptionManager, this) TextDocumentServiceHandler(project, this) - WorkspaceServiceHandler(project, this) + WorkspaceServiceHandler(project, lspInitResult, this) DefaultModuleDependenciesService(project, this) } } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt index a457a6e0ef5..3ef39c6073c 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt @@ -26,7 +26,9 @@ import org.eclipse.lsp4j.FileChangeType import org.eclipse.lsp4j.FileCreate import org.eclipse.lsp4j.FileDelete import org.eclipse.lsp4j.FileEvent +import org.eclipse.lsp4j.FileOperationFilter import org.eclipse.lsp4j.FileRename +import org.eclipse.lsp4j.InitializeResult import org.eclipse.lsp4j.RenameFilesParams import org.eclipse.lsp4j.TextDocumentIdentifier import org.eclipse.lsp4j.TextDocumentItem @@ -37,20 +39,22 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil.toU import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread import java.nio.file.FileSystems +import java.nio.file.PathMatcher import java.nio.file.Paths class WorkspaceServiceHandler( private val project: Project, + initializeResult: InitializeResult, serverInstance: Disposable, ) : BulkFileListener, ModuleRootListener { private var lastSnapshot: List = emptyList() - private val supportedFilePatterns = FileSystems.getDefault().getPathMatcher( - "glob:**/*.{ts,js,py,java}" - ) + private val operationMatchers: MutableMap>> = mutableMapOf() init { + operationMatchers.putAll(initializePatterns(initializeResult)) + project.messageBus.connect(serverInstance).subscribe( VirtualFileManager.VFS_CHANGES, this @@ -62,12 +66,46 @@ class WorkspaceServiceHandler( ) } + enum class FileOperationType { + CREATE, + DELETE, + RENAME, + } + + private fun initializePatterns(initializeResult: InitializeResult): Map>> { + val patterns = mutableMapOf>>() + + initializeResult.capabilities?.workspace?.fileOperations?.let { fileOps -> + patterns[FileOperationType.CREATE] = createMatchers(fileOps.didCreate?.filters) + patterns[FileOperationType.DELETE] = createMatchers(fileOps.didDelete?.filters) + patterns[FileOperationType.RENAME] = createMatchers(fileOps.didRename?.filters) + } + + return patterns + } + + private fun createMatchers(filters: List?): List> = + filters?.map { filter -> + FileSystems.getDefault().getPathMatcher("glob:${filter.pattern.glob}") to filter.pattern.matches + }.orEmpty() + + private fun shouldHandleFile(file: VirtualFile, operation: FileOperationType): Boolean { + val matchers = operationMatchers[operation] ?: return false + return matchers.any { (matcher, type) -> + when (type) { + "file" -> !file.isDirectory && matcher.matches(Paths.get(file.path)) + "folder" -> file.isDirectory && matcher.matches(Paths.get(file.path)) + else -> matcher.matches(Paths.get(file.path)) + } + } + } + private fun didCreateFiles(events: List) { AmazonQLspService.executeIfRunning(project) { languageServer -> val validFiles = events.mapNotNull { event -> when (event) { is VFileCopyEvent -> { - val newFile = event.newParent.findChild(event.newChildName)?.takeIf { shouldHandleFile(it) } + val newFile = event.newParent.findChild(event.newChildName)?.takeIf { shouldHandleFile(it, FileOperationType.CREATE) } ?: return@mapNotNull null toUriString(newFile)?.let { uri -> FileCreate().apply { @@ -76,7 +114,7 @@ class WorkspaceServiceHandler( } } else -> { - val file = event.file?.takeIf { shouldHandleFile(it) } + val file = event.file?.takeIf { shouldHandleFile(it, FileOperationType.CREATE) } ?: return@mapNotNull null toUriString(file)?.let { uri -> FileCreate().apply { @@ -102,11 +140,11 @@ class WorkspaceServiceHandler( val validFiles = events.mapNotNull { event -> when (event) { is VFileDeleteEvent -> { - val file = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null + val file = event.file.takeIf { shouldHandleFile(it, FileOperationType.DELETE) } ?: return@mapNotNull null toUriString(file) } is VFileMoveEvent -> { - val oldFile = event.oldParent?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null + val oldFile = event.oldParent?.takeIf { shouldHandleFile(it, FileOperationType.DELETE) } ?: return@mapNotNull null toUriString(oldFile) } else -> null @@ -132,7 +170,7 @@ class WorkspaceServiceHandler( val validRenames = events .filter { it.propertyName == VirtualFile.PROP_NAME } .mapNotNull { event -> - val renamedFile = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null + val renamedFile = event.file.takeIf { shouldHandleFile(it, FileOperationType.RENAME) } ?: return@mapNotNull null val oldFileName = event.oldValue as? String ?: return@mapNotNull null val parentFile = renamedFile.parent ?: return@mapNotNull null @@ -275,13 +313,4 @@ class WorkspaceServiceHandler( lastSnapshot = currentSnapshot } } - - private fun shouldHandleFile(file: VirtualFile): Boolean { - if (file.isDirectory) { - return true // Matches "**/*" with matches: "folder" - } - val path = Paths.get(file.path) - val result = supportedFilePatterns.matches(path) - return result - } } diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandlerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandlerTest.kt index 73a959d11b3..d2bde01ac89 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandlerTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandlerTest.kt @@ -35,8 +35,15 @@ import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams import org.eclipse.lsp4j.DidCloseTextDocumentParams import org.eclipse.lsp4j.DidOpenTextDocumentParams import org.eclipse.lsp4j.FileChangeType +import org.eclipse.lsp4j.FileOperationFilter +import org.eclipse.lsp4j.FileOperationOptions +import org.eclipse.lsp4j.FileOperationPattern +import org.eclipse.lsp4j.FileOperationsServerCapabilities +import org.eclipse.lsp4j.InitializeResult import org.eclipse.lsp4j.RenameFilesParams +import org.eclipse.lsp4j.ServerCapabilities import org.eclipse.lsp4j.WorkspaceFolder +import org.eclipse.lsp4j.WorkspaceServerCapabilities import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage import org.eclipse.lsp4j.services.TextDocumentService import org.eclipse.lsp4j.services.WorkspaceService @@ -52,11 +59,12 @@ import java.util.concurrent.CompletableFuture class WorkspaceServiceHandlerTest { private lateinit var project: Project + private lateinit var mockApplication: Application + private lateinit var mockInitializeResult: InitializeResult private lateinit var mockLanguageServer: AmazonQLanguageServer private lateinit var mockWorkspaceService: WorkspaceService private lateinit var mockTextDocumentService: TextDocumentService private lateinit var sut: WorkspaceServiceHandler - private lateinit var mockApplication: Application @BeforeEach fun setup() { @@ -107,7 +115,38 @@ class WorkspaceServiceHandlerTest { every { messageBus.connect(any()) } returns mockConnection every { mockConnection.subscribe(any(), any()) } just runs - sut = WorkspaceServiceHandler(project, mockk()) + // Mock InitializeResult with file operation patterns + mockInitializeResult = mockk() + val mockCapabilities = mockk() + val mockWorkspaceCapabilities = mockk() + val mockFileOperations = mockk() + + val fileFilter = FileOperationFilter().apply { + pattern = FileOperationPattern().apply { + glob = "**/*.{ts,js,py,java}" + matches = "file" + } + } + val folderFilter = FileOperationFilter().apply { + pattern = FileOperationPattern().apply { + glob = "**/*" + matches = "folder" + } + } + + val fileOperationOptions = FileOperationOptions().apply { + filters = listOf(fileFilter, folderFilter) + } + + every { mockFileOperations.didCreate } returns fileOperationOptions + every { mockFileOperations.didDelete } returns fileOperationOptions + every { mockFileOperations.didRename } returns fileOperationOptions + every { mockWorkspaceCapabilities.fileOperations } returns mockFileOperations + every { mockCapabilities.workspace } returns mockWorkspaceCapabilities + every { mockInitializeResult.capabilities } returns mockCapabilities + + // Create WorkspaceServiceHandler with mocked InitializeResult + sut = WorkspaceServiceHandler(project, mockInitializeResult, mockk()) } @Test