-
Notifications
You must be signed in to change notification settings - Fork 251
feat(amazonq): implement workspace file messages #5377
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 23 commits
686fda2
a5250b5
0efb633
788e833
429f5e9
aababb6
c3d855a
f826372
fad4467
ffa167a
94c25e4
87817ea
d495450
7cff72e
70b7e53
eaa993a
d139dc4
5458f0c
0628cae
3f2a98d
401826d
780395e
8ee75f8
de16193
ec5bfbe
62adbc1
450e416
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package software.aws.toolkits.jetbrains.services.amazonq.lsp.util | ||
|
||
import com.intellij.openapi.project.Project | ||
import com.intellij.openapi.roots.ProjectRootManager | ||
import org.eclipse.lsp4j.WorkspaceFolder | ||
|
||
object WorkspaceFolderUtil { | ||
fun createWorkspaceFolders(project: Project): List<WorkspaceFolder> = | ||
if (project.isDefault) { | ||
emptyList() | ||
Check warning on line 13 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt
|
||
} else { | ||
ProjectRootManager.getInstance(project).contentRoots.map { contentRoot -> | ||
WorkspaceFolder().apply { | ||
name = contentRoot.name | ||
this.uri = contentRoot.url | ||
} | ||
Check warning on line 19 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt
|
||
} | ||
} | ||
Check warning on line 21 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package software.aws.toolkits.jetbrains.services.amazonq.lsp.workspace | ||
|
||
import com.intellij.openapi.Disposable | ||
import com.intellij.openapi.project.Project | ||
import com.intellij.openapi.roots.ModuleRootEvent | ||
import com.intellij.openapi.roots.ModuleRootListener | ||
import com.intellij.openapi.vfs.VirtualFile | ||
import com.intellij.openapi.vfs.VirtualFileManager | ||
import com.intellij.openapi.vfs.newvfs.BulkFileListener | ||
import com.intellij.openapi.vfs.newvfs.events.VFileCreateEvent | ||
import com.intellij.openapi.vfs.newvfs.events.VFileDeleteEvent | ||
import com.intellij.openapi.vfs.newvfs.events.VFileEvent | ||
import org.eclipse.lsp4j.CreateFilesParams | ||
import org.eclipse.lsp4j.DeleteFilesParams | ||
import org.eclipse.lsp4j.DidChangeWatchedFilesParams | ||
import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams | ||
import org.eclipse.lsp4j.FileChangeType | ||
import org.eclipse.lsp4j.FileCreate | ||
import org.eclipse.lsp4j.FileDelete | ||
import org.eclipse.lsp4j.FileEvent | ||
import org.eclipse.lsp4j.WorkspaceFolder | ||
import org.eclipse.lsp4j.WorkspaceFoldersChangeEvent | ||
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService | ||
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.Paths | ||
|
||
class WorkspaceServiceHandler( | ||
private val project: Project, | ||
Check warning on line 33 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
serverInstance: Disposable, | ||
) : BulkFileListener, | ||
ModuleRootListener { | ||
|
||
private var lastSnapshot: List<WorkspaceFolder> = emptyList() | ||
private val supportedFilePatterns = FileSystems.getDefault().getPathMatcher( | ||
"glob:**/*.{ts,js,py,java}" | ||
Check warning on line 40 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
Comment on lines
+42
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should read the patterns out from the server capabilities returned from the initialization message |
||
|
||
init { | ||
project.messageBus.connect(serverInstance).subscribe( | ||
VirtualFileManager.VFS_CHANGES, | ||
this | ||
Check warning on line 46 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
|
||
project.messageBus.connect(serverInstance).subscribe( | ||
ModuleRootListener.TOPIC, | ||
this | ||
Check warning on line 51 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
} | ||
Check warning on line 53 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
private fun didCreateFiles(events: List<VFileEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validFiles = events.mapNotNull { event -> | ||
Check warning on line 57 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
val file = event.file?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null | ||
file.toNioPath().toUri().toString().takeIf { it.isNotEmpty() }?.let { uri -> | ||
FileCreate().apply { | ||
this.uri = uri | ||
} | ||
} | ||
Check warning on line 63 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
} | ||
|
||
if (validFiles.isNotEmpty()) { | ||
languageServer.workspaceService.didCreateFiles( | ||
CreateFilesParams().apply { | ||
files = validFiles | ||
} | ||
Check warning on line 70 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
} | ||
} | ||
} | ||
Check warning on line 74 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
private fun didDeleteFiles(events: List<VFileEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validFiles = events.mapNotNull { event -> | ||
Check warning on line 78 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
val file = event.file?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null | ||
file.toNioPath().toUri().toString().takeIf { it.isNotEmpty() }?.let { uri -> | ||
FileDelete().apply { | ||
this.uri = uri | ||
} | ||
} | ||
Check warning on line 84 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
} | ||
|
||
if (validFiles.isNotEmpty()) { | ||
languageServer.workspaceService.didDeleteFiles( | ||
DeleteFilesParams().apply { | ||
files = validFiles | ||
} | ||
Check warning on line 91 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
} | ||
} | ||
} | ||
Check warning on line 95 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
private fun didChangeWatchedFiles(events: List<VFileEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validChanges = events.mapNotNull { event -> | ||
Check warning on line 99 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
event.file?.toNioPath()?.toUri()?.toString()?.takeIf { it.isNotEmpty() }?.let { uri -> | ||
FileEvent().apply { | ||
this.uri = uri | ||
type = when (event) { | ||
Check warning on line 103 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
is VFileCreateEvent -> FileChangeType.Created | ||
is VFileDeleteEvent -> FileChangeType.Deleted | ||
else -> FileChangeType.Changed | ||
Check warning on line 106 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
} | ||
} | ||
} | ||
Check warning on line 109 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
} | ||
|
||
if (validChanges.isNotEmpty()) { | ||
languageServer.workspaceService.didChangeWatchedFiles( | ||
DidChangeWatchedFilesParams().apply { | ||
changes = validChanges | ||
} | ||
Check warning on line 116 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
} | ||
} | ||
} | ||
Check warning on line 120 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
override fun after(events: List<VFileEvent>) { | ||
// since we are using synchronous FileListener | ||
pluginAwareExecuteOnPooledThread { | ||
didCreateFiles(events.filterIsInstance<VFileCreateEvent>()) | ||
didDeleteFiles(events.filterIsInstance<VFileDeleteEvent>()) | ||
didChangeWatchedFiles(events) | ||
} | ||
} | ||
Check warning on line 129 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
override fun beforeRootsChange(event: ModuleRootEvent) { | ||
lastSnapshot = createWorkspaceFolders(project) | ||
} | ||
Check warning on line 133 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
override fun rootsChanged(event: ModuleRootEvent) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val currentSnapshot = createWorkspaceFolders(project) | ||
val addedFolders = currentSnapshot.filter { folder -> lastSnapshot.none { it.uri == folder.uri } } | ||
val removedFolders = lastSnapshot.filter { folder -> currentSnapshot.none { it.uri == folder.uri } } | ||
Check warning on line 139 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
if (addedFolders.isNotEmpty() || removedFolders.isNotEmpty()) { | ||
languageServer.workspaceService.didChangeWorkspaceFolders( | ||
DidChangeWorkspaceFoldersParams().apply { | ||
this.event = WorkspaceFoldersChangeEvent().apply { | ||
added = addedFolders | ||
removed = removedFolders | ||
} | ||
} | ||
Check warning on line 148 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
) | ||
} | ||
|
||
lastSnapshot = currentSnapshot | ||
} | ||
} | ||
Check warning on line 154 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
|
||
private fun shouldHandleFile(file: VirtualFile): Boolean { | ||
if (file.isDirectory) { | ||
return true // Matches "**/*" with matches: "folder" | ||
Check warning on line 158 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
} | ||
val path = Paths.get(file.path) | ||
val result = supportedFilePatterns.matches(path) | ||
return result | ||
Check warning on line 162 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
|
||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
// SPDX-License-Identifier: Apache-2.0 | ||
|
||
package software.aws.toolkits.jetbrains.services.amazonq.lsp.util | ||
|
||
import com.intellij.openapi.project.Project | ||
import com.intellij.openapi.roots.ProjectRootManager | ||
import com.intellij.openapi.vfs.VirtualFile | ||
import io.mockk.every | ||
import io.mockk.mockk | ||
import org.junit.Assert.assertEquals | ||
import org.junit.Test | ||
|
||
class WorkspaceFolderUtilTest { | ||
|
||
@Test | ||
fun `createWorkspaceFolders returns empty list when no workspace folders`() { | ||
val mockProject = mockk<Project>() | ||
every { mockProject.isDefault } returns true | ||
|
||
val result = WorkspaceFolderUtil.createWorkspaceFolders(mockProject) | ||
|
||
assertEquals(emptyList<VirtualFile>(), result) | ||
} | ||
|
||
@Test | ||
fun `createWorkspaceFolders returns workspace folders for non-default project`() { | ||
val mockProject = mockk<Project>() | ||
val mockProjectRootManager = mockk<ProjectRootManager>() | ||
val mockContentRoot1 = mockk<VirtualFile>() | ||
val mockContentRoot2 = mockk<VirtualFile>() | ||
|
||
every { mockProject.isDefault } returns false | ||
every { ProjectRootManager.getInstance(mockProject) } returns mockProjectRootManager | ||
every { mockProjectRootManager.contentRoots } returns arrayOf(mockContentRoot1, mockContentRoot2) | ||
|
||
every { mockContentRoot1.name } returns "root1" | ||
every { mockContentRoot1.url } returns "file:///path/to/root1" | ||
every { mockContentRoot2.name } returns "root2" | ||
every { mockContentRoot2.url } returns "file:///path/to/root2" | ||
|
||
val result = WorkspaceFolderUtil.createWorkspaceFolders(mockProject) | ||
|
||
assertEquals(2, result.size) | ||
assertEquals("file:///path/to/root1", result[0].uri) | ||
assertEquals("file:///path/to/root2", result[1].uri) | ||
assertEquals("root1", result[0].name) | ||
assertEquals("root2", result[1].name) | ||
} | ||
|
||
@Test | ||
fun `reateWorkspaceFolders returns empty list when project has no content roots`() { | ||
val mockProject = mockk<Project>() | ||
val mockProjectRootManager = mockk<ProjectRootManager>() | ||
|
||
every { mockProject.isDefault } returns false | ||
every { ProjectRootManager.getInstance(mockProject) } returns mockProjectRootManager | ||
every { mockProjectRootManager.contentRoots } returns emptyArray() | ||
|
||
val result = WorkspaceFolderUtil.createWorkspaceFolders(mockProject) | ||
|
||
assertEquals(emptyList<VirtualFile>(), result) | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.