Skip to content

Commit 20c045b

Browse files
feat(amazonq): fetch filePatterns from initializeResponse for workspace messages (#5457)
Utilize InitializeResponse's ServerCapabilities object to get the accepted file patterns for each supported message our workspaceService is handling.
1 parent 22431f0 commit 20c045b

File tree

3 files changed

+89
-21
lines changed

3 files changed

+89
-21
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -378,15 +378,15 @@ private class AmazonQServerInstance(private val project: Project, private val cs
378378
}
379379

380380
// invokeOnCompletion results in weird lock/timeout error
381-
initializeResult.asCompletableFuture().handleAsync { r, ex ->
381+
initializeResult.asCompletableFuture().handleAsync { lspInitResult, ex ->
382382
if (ex != null) {
383383
return@handleAsync
384384
}
385385

386386
this@AmazonQServerInstance.apply {
387387
DefaultAuthCredentialsService(project, encryptionManager, this)
388388
TextDocumentServiceHandler(project, this)
389-
WorkspaceServiceHandler(project, this)
389+
WorkspaceServiceHandler(project, lspInitResult, this)
390390
DefaultModuleDependenciesService(project, this)
391391
}
392392
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt

Lines changed: 46 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ import org.eclipse.lsp4j.FileChangeType
2626
import org.eclipse.lsp4j.FileCreate
2727
import org.eclipse.lsp4j.FileDelete
2828
import org.eclipse.lsp4j.FileEvent
29+
import org.eclipse.lsp4j.FileOperationFilter
2930
import org.eclipse.lsp4j.FileRename
31+
import org.eclipse.lsp4j.InitializeResult
3032
import org.eclipse.lsp4j.RenameFilesParams
3133
import org.eclipse.lsp4j.TextDocumentIdentifier
3234
import org.eclipse.lsp4j.TextDocumentItem
@@ -37,20 +39,22 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil.toU
3739
import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.WorkspaceFolderUtil.createWorkspaceFolders
3840
import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread
3941
import java.nio.file.FileSystems
42+
import java.nio.file.PathMatcher
4043
import java.nio.file.Paths
4144

4245
class WorkspaceServiceHandler(
4346
private val project: Project,
47+
initializeResult: InitializeResult,
4448
serverInstance: Disposable,
4549
) : BulkFileListener,
4650
ModuleRootListener {
4751

4852
private var lastSnapshot: List<WorkspaceFolder> = emptyList()
49-
private val supportedFilePatterns = FileSystems.getDefault().getPathMatcher(
50-
"glob:**/*.{ts,js,py,java}"
51-
)
53+
private val operationMatchers: MutableMap<FileOperationType, List<Pair<PathMatcher, String>>> = mutableMapOf()
5254

5355
init {
56+
operationMatchers.putAll(initializePatterns(initializeResult))
57+
5458
project.messageBus.connect(serverInstance).subscribe(
5559
VirtualFileManager.VFS_CHANGES,
5660
this
@@ -62,12 +66,46 @@ class WorkspaceServiceHandler(
6266
)
6367
}
6468

69+
enum class FileOperationType {
70+
CREATE,
71+
DELETE,
72+
RENAME,
73+
}
74+
75+
private fun initializePatterns(initializeResult: InitializeResult): Map<FileOperationType, List<Pair<PathMatcher, String>>> {
76+
val patterns = mutableMapOf<FileOperationType, List<Pair<PathMatcher, String>>>()
77+
78+
initializeResult.capabilities?.workspace?.fileOperations?.let { fileOps ->
79+
patterns[FileOperationType.CREATE] = createMatchers(fileOps.didCreate?.filters)
80+
patterns[FileOperationType.DELETE] = createMatchers(fileOps.didDelete?.filters)
81+
patterns[FileOperationType.RENAME] = createMatchers(fileOps.didRename?.filters)
82+
}
83+
84+
return patterns
85+
}
86+
87+
private fun createMatchers(filters: List<FileOperationFilter>?): List<Pair<PathMatcher, String>> =
88+
filters?.map { filter ->
89+
FileSystems.getDefault().getPathMatcher("glob:${filter.pattern.glob}") to filter.pattern.matches
90+
}.orEmpty()
91+
92+
private fun shouldHandleFile(file: VirtualFile, operation: FileOperationType): Boolean {
93+
val matchers = operationMatchers[operation] ?: return false
94+
return matchers.any { (matcher, type) ->
95+
when (type) {
96+
"file" -> !file.isDirectory && matcher.matches(Paths.get(file.path))
97+
"folder" -> file.isDirectory && matcher.matches(Paths.get(file.path))
98+
else -> matcher.matches(Paths.get(file.path))
99+
}
100+
}
101+
}
102+
65103
private fun didCreateFiles(events: List<VFileEvent>) {
66104
AmazonQLspService.executeIfRunning(project) { languageServer ->
67105
val validFiles = events.mapNotNull { event ->
68106
when (event) {
69107
is VFileCopyEvent -> {
70-
val newFile = event.newParent.findChild(event.newChildName)?.takeIf { shouldHandleFile(it) }
108+
val newFile = event.newParent.findChild(event.newChildName)?.takeIf { shouldHandleFile(it, FileOperationType.CREATE) }
71109
?: return@mapNotNull null
72110
toUriString(newFile)?.let { uri ->
73111
FileCreate().apply {
@@ -76,7 +114,7 @@ class WorkspaceServiceHandler(
76114
}
77115
}
78116
else -> {
79-
val file = event.file?.takeIf { shouldHandleFile(it) }
117+
val file = event.file?.takeIf { shouldHandleFile(it, FileOperationType.CREATE) }
80118
?: return@mapNotNull null
81119
toUriString(file)?.let { uri ->
82120
FileCreate().apply {
@@ -102,11 +140,11 @@ class WorkspaceServiceHandler(
102140
val validFiles = events.mapNotNull { event ->
103141
when (event) {
104142
is VFileDeleteEvent -> {
105-
val file = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
143+
val file = event.file.takeIf { shouldHandleFile(it, FileOperationType.DELETE) } ?: return@mapNotNull null
106144
toUriString(file)
107145
}
108146
is VFileMoveEvent -> {
109-
val oldFile = event.oldParent?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
147+
val oldFile = event.oldParent?.takeIf { shouldHandleFile(it, FileOperationType.DELETE) } ?: return@mapNotNull null
110148
toUriString(oldFile)
111149
}
112150
else -> null
@@ -132,7 +170,7 @@ class WorkspaceServiceHandler(
132170
val validRenames = events
133171
.filter { it.propertyName == VirtualFile.PROP_NAME }
134172
.mapNotNull { event ->
135-
val renamedFile = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
173+
val renamedFile = event.file.takeIf { shouldHandleFile(it, FileOperationType.RENAME) } ?: return@mapNotNull null
136174
val oldFileName = event.oldValue as? String ?: return@mapNotNull null
137175
val parentFile = renamedFile.parent ?: return@mapNotNull null
138176

@@ -275,13 +313,4 @@ class WorkspaceServiceHandler(
275313
lastSnapshot = currentSnapshot
276314
}
277315
}
278-
279-
private fun shouldHandleFile(file: VirtualFile): Boolean {
280-
if (file.isDirectory) {
281-
return true // Matches "**/*" with matches: "folder"
282-
}
283-
val path = Paths.get(file.path)
284-
val result = supportedFilePatterns.matches(path)
285-
return result
286-
}
287316
}

plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandlerTest.kt

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,15 @@ import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams
3535
import org.eclipse.lsp4j.DidCloseTextDocumentParams
3636
import org.eclipse.lsp4j.DidOpenTextDocumentParams
3737
import org.eclipse.lsp4j.FileChangeType
38+
import org.eclipse.lsp4j.FileOperationFilter
39+
import org.eclipse.lsp4j.FileOperationOptions
40+
import org.eclipse.lsp4j.FileOperationPattern
41+
import org.eclipse.lsp4j.FileOperationsServerCapabilities
42+
import org.eclipse.lsp4j.InitializeResult
3843
import org.eclipse.lsp4j.RenameFilesParams
44+
import org.eclipse.lsp4j.ServerCapabilities
3945
import org.eclipse.lsp4j.WorkspaceFolder
46+
import org.eclipse.lsp4j.WorkspaceServerCapabilities
4047
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
4148
import org.eclipse.lsp4j.services.TextDocumentService
4249
import org.eclipse.lsp4j.services.WorkspaceService
@@ -52,11 +59,12 @@ import java.util.concurrent.CompletableFuture
5259

5360
class WorkspaceServiceHandlerTest {
5461
private lateinit var project: Project
62+
private lateinit var mockApplication: Application
63+
private lateinit var mockInitializeResult: InitializeResult
5564
private lateinit var mockLanguageServer: AmazonQLanguageServer
5665
private lateinit var mockWorkspaceService: WorkspaceService
5766
private lateinit var mockTextDocumentService: TextDocumentService
5867
private lateinit var sut: WorkspaceServiceHandler
59-
private lateinit var mockApplication: Application
6068

6169
@BeforeEach
6270
fun setup() {
@@ -107,7 +115,38 @@ class WorkspaceServiceHandlerTest {
107115
every { messageBus.connect(any<Disposable>()) } returns mockConnection
108116
every { mockConnection.subscribe(any(), any()) } just runs
109117

110-
sut = WorkspaceServiceHandler(project, mockk())
118+
// Mock InitializeResult with file operation patterns
119+
mockInitializeResult = mockk<InitializeResult>()
120+
val mockCapabilities = mockk<ServerCapabilities>()
121+
val mockWorkspaceCapabilities = mockk<WorkspaceServerCapabilities>()
122+
val mockFileOperations = mockk<FileOperationsServerCapabilities>()
123+
124+
val fileFilter = FileOperationFilter().apply {
125+
pattern = FileOperationPattern().apply {
126+
glob = "**/*.{ts,js,py,java}"
127+
matches = "file"
128+
}
129+
}
130+
val folderFilter = FileOperationFilter().apply {
131+
pattern = FileOperationPattern().apply {
132+
glob = "**/*"
133+
matches = "folder"
134+
}
135+
}
136+
137+
val fileOperationOptions = FileOperationOptions().apply {
138+
filters = listOf(fileFilter, folderFilter)
139+
}
140+
141+
every { mockFileOperations.didCreate } returns fileOperationOptions
142+
every { mockFileOperations.didDelete } returns fileOperationOptions
143+
every { mockFileOperations.didRename } returns fileOperationOptions
144+
every { mockWorkspaceCapabilities.fileOperations } returns mockFileOperations
145+
every { mockCapabilities.workspace } returns mockWorkspaceCapabilities
146+
every { mockInitializeResult.capabilities } returns mockCapabilities
147+
148+
// Create WorkspaceServiceHandler with mocked InitializeResult
149+
sut = WorkspaceServiceHandler(project, mockInitializeResult, mockk())
111150
}
112151

113152
@Test

0 commit comments

Comments
 (0)