-
Notifications
You must be signed in to change notification settings - Fork 263
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
Merged
Merged
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
686fda2
add workspace handler
samgst-amazon a5250b5
break up repeat code
samgst-amazon 0efb633
use serverInstance as messageBus disposable
samgst-amazon 788e833
init starts listeners
samgst-amazon 429f5e9
update listeners
samgst-amazon aababb6
fix init params
samgst-amazon c3d855a
didChangeWatchedFiles impl
samgst-amazon f826372
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon fad4467
detekt
samgst-amazon ffa167a
move executeIfRunning
samgst-amazon 94c25e4
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon 87817ea
private class
samgst-amazon d495450
null uri handling
samgst-amazon 7cff72e
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon 70b7e53
didChangeWorkspaceFolders
samgst-amazon eaa993a
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon d139dc4
detekt
samgst-amazon 5458f0c
add tests for changeWorkspaceFolders
samgst-amazon 0628cae
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon 3f2a98d
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon 401826d
fix test
samgst-amazon 780395e
glob pattern matching
samgst-amazon 8ee75f8
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon de16193
add didRename
samgst-amazon ec5bfbe
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon 62adbc1
detekt fixes
samgst-amazon 450e416
Merge branch 'feature/q-lsp' into samgst/lsp-WSmessages
samgst-amazon File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
22 changes: 22 additions & 0 deletions
22
...nity/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtil.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} else { | ||
ProjectRootManager.getInstance(project).contentRoots.map { contentRoot -> | ||
WorkspaceFolder().apply { | ||
name = contentRoot.name | ||
this.uri = contentRoot.url | ||
} | ||
} | ||
} | ||
} |
198 changes: 198 additions & 0 deletions
198
...software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
// 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 com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent | ||
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.FileRename | ||
import org.eclipse.lsp4j.RenameFilesParams | ||
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, | ||
serverInstance: Disposable, | ||
) : BulkFileListener, | ||
ModuleRootListener { | ||
|
||
private var lastSnapshot: List<WorkspaceFolder> = emptyList() | ||
private val supportedFilePatterns = FileSystems.getDefault().getPathMatcher( | ||
"glob:**/*.{ts,js,py,java}" | ||
) | ||
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 | ||
) | ||
|
||
project.messageBus.connect(serverInstance).subscribe( | ||
ModuleRootListener.TOPIC, | ||
this | ||
) | ||
} | ||
|
||
private fun didCreateFiles(events: List<VFileEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validFiles = events.mapNotNull { event -> | ||
val file = event.file?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null | ||
file.toNioPath().toUri().toString().takeIf { it.isNotEmpty() }?.let { uri -> | ||
FileCreate().apply { | ||
this.uri = uri | ||
} | ||
} | ||
} | ||
|
||
if (validFiles.isNotEmpty()) { | ||
languageServer.workspaceService.didCreateFiles( | ||
CreateFilesParams().apply { | ||
files = validFiles | ||
} | ||
) | ||
} | ||
} | ||
} | ||
|
||
private fun didDeleteFiles(events: List<VFileEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validFiles = events.mapNotNull { event -> | ||
val file = event.file?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null | ||
file.toNioPath().toUri().toString().takeIf { it.isNotEmpty() }?.let { uri -> | ||
FileDelete().apply { | ||
this.uri = uri | ||
} | ||
} | ||
} | ||
|
||
if (validFiles.isNotEmpty()) { | ||
languageServer.workspaceService.didDeleteFiles( | ||
DeleteFilesParams().apply { | ||
files = validFiles | ||
} | ||
) | ||
} | ||
} | ||
} | ||
|
||
private fun didRenameFiles(events: List<VFilePropertyChangeEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validRenames = events | ||
.filter { it.propertyName == VirtualFile.PROP_NAME } | ||
.mapNotNull { event -> | ||
val file = event.file.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null | ||
val oldName = event.oldValue as? String ?: return@mapNotNull null | ||
if (event.newValue !is String) return@mapNotNull null | ||
|
||
// Construct old and new URIs | ||
val parentPath = file.parent?.toNioPath() ?: return@mapNotNull null | ||
val oldUri = parentPath.resolve(oldName).toUri().toString() | ||
val newUri = file.toNioPath().toUri().toString() | ||
|
||
FileRename().apply { | ||
this.oldUri = oldUri | ||
this.newUri = newUri | ||
} | ||
} | ||
|
||
if (validRenames.isNotEmpty()) { | ||
languageServer.workspaceService.didRenameFiles( | ||
RenameFilesParams().apply { | ||
files = validRenames | ||
} | ||
) | ||
} | ||
} | ||
} | ||
|
||
private fun didChangeWatchedFiles(events: List<VFileEvent>) { | ||
AmazonQLspService.executeIfRunning(project) { languageServer -> | ||
val validChanges = events.mapNotNull { event -> | ||
event.file?.toNioPath()?.toUri()?.toString()?.takeIf { it.isNotEmpty() }?.let { uri -> | ||
FileEvent().apply { | ||
this.uri = uri | ||
type = when (event) { | ||
is VFileCreateEvent -> FileChangeType.Created | ||
is VFileDeleteEvent -> FileChangeType.Deleted | ||
else -> FileChangeType.Changed | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (validChanges.isNotEmpty()) { | ||
languageServer.workspaceService.didChangeWatchedFiles( | ||
DidChangeWatchedFilesParams().apply { | ||
changes = validChanges | ||
} | ||
) | ||
} | ||
} | ||
} | ||
|
||
override fun after(events: List<VFileEvent>) { | ||
// since we are using synchronous FileListener | ||
pluginAwareExecuteOnPooledThread { | ||
didCreateFiles(events.filterIsInstance<VFileCreateEvent>()) | ||
didDeleteFiles(events.filterIsInstance<VFileDeleteEvent>()) | ||
didRenameFiles(events.filterIsInstance<VFilePropertyChangeEvent>()) | ||
didChangeWatchedFiles(events) | ||
} | ||
} | ||
|
||
override fun beforeRootsChange(event: ModuleRootEvent) { | ||
lastSnapshot = createWorkspaceFolders(project) | ||
} | ||
|
||
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 } } | ||
|
||
if (addedFolders.isNotEmpty() || removedFolders.isNotEmpty()) { | ||
languageServer.workspaceService.didChangeWorkspaceFolders( | ||
DidChangeWorkspaceFoldersParams().apply { | ||
this.event = WorkspaceFoldersChangeEvent().apply { | ||
added = addedFolders | ||
removed = removedFolders | ||
} | ||
} | ||
) | ||
} | ||
|
||
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 | ||
} | ||
} |
64 changes: 64 additions & 0 deletions
64
.../tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/WorkspaceFolderUtilTest.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.jupiter.api.Assertions.assertEquals | ||
import org.junit.jupiter.api.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) | ||
} | ||
} |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.