Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@
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.VFileCopyEvent
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.VFileMoveEvent
import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
import org.eclipse.lsp4j.CreateFilesParams
import org.eclipse.lsp4j.DeleteFilesParams
Expand Down Expand Up @@ -59,10 +61,24 @@
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
toUriString(file)?.let { uri ->
FileCreate().apply {
this.uri = uri
when (event) {
is VFileCopyEvent -> {
val newFile = event.newParent.findChild(event.newChildName)?.takeIf { shouldHandleFile(it) }
?: return@mapNotNull null
toUriString(newFile)?.let { uri ->
FileCreate().apply {
this.uri = uri
}
}
}
else -> {
val file = event.file?.takeIf { shouldHandleFile(it) }
?: return@mapNotNull null
toUriString(file)?.let { uri ->
FileCreate().apply {
this.uri = uri
}
}
}
}
}
Expand All @@ -80,8 +96,17 @@
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
toUriString(file)?.let { uri ->
when (event) {
is VFileDeleteEvent -> {
val file = event.file?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null

Check warning on line 101 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/workspace/WorkspaceServiceHandler.kt

View workflow job for this annotation

GitHub Actions / Qodana Community for JVM

Usage of redundant or deprecated syntax or deprecated symbols

Unnecessary safe call on a non-null receiver of type VirtualFile
toUriString(file)
}
is VFileMoveEvent -> {
val oldFile = event.oldParent?.takeIf { shouldHandleFile(it) } ?: return@mapNotNull null
toUriString(oldFile)
}
else -> null
}?.let { uri ->
FileDelete().apply {
this.uri = uri
}
Expand Down Expand Up @@ -129,15 +154,47 @@

private fun didChangeWatchedFiles(events: List<VFileEvent>) {
AmazonQLspService.executeIfRunning(project) { languageServer ->
val validChanges = events.mapNotNull { event ->
event.file?.let { toUriString(it) }?.let { uri ->
FileEvent().apply {
this.uri = uri
type = when (event) {
is VFileCreateEvent -> FileChangeType.Created
is VFileDeleteEvent -> FileChangeType.Deleted
else -> FileChangeType.Changed
}
val validChanges = events.flatMap { event ->
when (event) {
is VFileCopyEvent -> {
event.newParent.findChild(event.newChildName)?.let { newFile ->
toUriString(newFile)?.let { uri ->
listOf(FileEvent().apply {
this.uri = uri
type = FileChangeType.Created
})
}
} ?: emptyList()
}
is VFileMoveEvent -> {
listOfNotNull(
toUriString(event.oldParent)?.let { oldUri ->
FileEvent().apply {
uri = oldUri
type = FileChangeType.Deleted
}
},
toUriString(event.file)?.let { newUri ->
FileEvent().apply {
uri = newUri
type = FileChangeType.Created
}
}
)
}
else -> {
event.file?.let { file ->
toUriString(file)?.let { uri ->
listOf(FileEvent().apply {
this.uri = uri
type = when (event) {
is VFileCreateEvent -> FileChangeType.Created
is VFileDeleteEvent -> FileChangeType.Deleted
else -> FileChangeType.Changed
}
})
}
} ?: emptyList()
}
}
}
Expand All @@ -155,8 +212,8 @@
override fun after(events: List<VFileEvent>) {
// since we are using synchronous FileListener
pluginAwareExecuteOnPooledThread {
didCreateFiles(events.filterIsInstance<VFileCreateEvent>())
didDeleteFiles(events.filterIsInstance<VFileDeleteEvent>())
didCreateFiles(events.filter { it is VFileCreateEvent || it is VFileMoveEvent || it is VFileCopyEvent })
didDeleteFiles(events.filter { it is VFileMoveEvent || it is VFileDeleteEvent })
didRenameFiles(events.filterIsInstance<VFilePropertyChangeEvent>())
didChangeWatchedFiles(events)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.serviceIfCreated
import com.intellij.openapi.project.Project
import com.intellij.openapi.vfs.VirtualFile
import com.intellij.openapi.vfs.newvfs.events.VFileCopyEvent
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.VFileMoveEvent
import com.intellij.openapi.vfs.newvfs.events.VFilePropertyChangeEvent
import com.intellij.util.messages.MessageBus
import com.intellij.util.messages.MessageBusConnection
Expand Down Expand Up @@ -167,6 +169,32 @@ class WorkspaceServiceHandlerTest {
verify(exactly = 0) { mockWorkspaceService.didCreateFiles(any()) }
}

@Test
fun `test didCreateFiles with move event`() = runTest {
val oldUri = URI("file:///test/oldPath")
val newUri = URI("file:///test/newPath")
val moveEvent = createMockVFileMoveEvent(oldUri, newUri, "test.py")

sut.after(listOf(moveEvent))

val paramsSlot = slot<CreateFilesParams>()
verify { mockWorkspaceService.didCreateFiles(capture(paramsSlot)) }
assertEquals(normalizeFileUri(newUri.toString()), paramsSlot.captured.files[0].uri)
}

@Test
fun `test didCreateFiles with copy event`() = runTest {
val originalUri = URI("file:///test/original")
val newUri = URI("file:///test/new")
val copyEvent = createMockVFileCopyEvent(originalUri, newUri, "test.py")

sut.after(listOf(copyEvent))

val paramsSlot = slot<CreateFilesParams>()
verify { mockWorkspaceService.didCreateFiles(capture(paramsSlot)) }
assertEquals(normalizeFileUri(newUri.toString()), paramsSlot.captured.files[0].uri)
}

@Test
fun `test didDeleteFiles with Python file`() = runTest {
val pyUri = URI("file:///test/path")
Expand Down Expand Up @@ -237,6 +265,49 @@ class WorkspaceServiceHandlerTest {
assertEquals(normalizeFileUri(dirUri.toString()), paramsSlot.captured.files[0].uri)
}


@Test
fun `test didDeleteFiles handles both delete and move events in same batch`() = runTest {
val deleteUri = URI("file:///test/deleteFile")
val oldMoveUri = URI("file:///test/oldMoveFile")
val newMoveUri = URI("file:///test/newMoveFile")

val deleteEvent = createMockVFileEvent(deleteUri, FileChangeType.Deleted, false, "py")
val moveEvent = createMockVFileMoveEvent(oldMoveUri, newMoveUri, "test.py")

sut.after(listOf(deleteEvent, moveEvent))

val deleteParamsSlot = slot<DeleteFilesParams>()
verify { mockWorkspaceService.didDeleteFiles(capture(deleteParamsSlot)) }
assertEquals(2, deleteParamsSlot.captured.files.size)
assertEquals(normalizeFileUri(deleteUri.toString()), deleteParamsSlot.captured.files[0].uri)
assertEquals(normalizeFileUri(oldMoveUri.toString()), deleteParamsSlot.captured.files[1].uri)
}

@Test
fun `test didDeleteFiles with move event of unsupported file type`() = runTest {
val oldUri = URI("file:///test/oldPath")
val newUri = URI("file:///test/newPath")
val moveEvent = createMockVFileMoveEvent(oldUri, newUri, "test.txt")

sut.after(listOf(moveEvent))

verify(exactly = 0) { mockWorkspaceService.didDeleteFiles(any()) }
}

@Test
fun `test didDeleteFiles with move event of directory`() = runTest {
val oldUri = URI("file:///test/oldDir")
val newUri = URI("file:///test/newDir")
val moveEvent = createMockVFileMoveEvent(oldUri, newUri, "", true)

sut.after(listOf(moveEvent))

val deleteParamsSlot = slot<DeleteFilesParams>()
verify { mockWorkspaceService.didDeleteFiles(capture(deleteParamsSlot)) }
assertEquals(normalizeFileUri(oldUri.toString()), deleteParamsSlot.captured.files[0].uri)
}

@Test
fun `test didChangeWatchedFiles with valid events`() = runTest {
// Arrange
Expand All @@ -262,6 +333,38 @@ class WorkspaceServiceHandlerTest {
assertEquals(FileChangeType.Changed, paramsSlot.captured.changes[2].type)
}

@Test
fun `test didChangeWatchedFiles with move event reports both delete and create`() = runTest {
val oldUri = URI("file:///test/oldPath")
val newUri = URI("file:///test/newPath")
val moveEvent = createMockVFileMoveEvent(oldUri, newUri, "test.py")

sut.after(listOf(moveEvent))

val paramsSlot = slot<DidChangeWatchedFilesParams>()
verify { mockWorkspaceService.didChangeWatchedFiles(capture(paramsSlot)) }

assertEquals(2, paramsSlot.captured.changes.size)
assertEquals(normalizeFileUri(oldUri.toString()), paramsSlot.captured.changes[0].uri)
assertEquals(FileChangeType.Deleted, paramsSlot.captured.changes[0].type)
assertEquals(normalizeFileUri(newUri.toString()), paramsSlot.captured.changes[1].uri)
assertEquals(FileChangeType.Created, paramsSlot.captured.changes[1].type)
}

@Test
fun `test didChangeWatchedFiles with copy event`() = runTest {
val originalUri = URI("file:///test/original")
val newUri = URI("file:///test/new")
val copyEvent = createMockVFileCopyEvent(originalUri, newUri, "test.py")

sut.after(listOf(copyEvent))

val paramsSlot = slot<DidChangeWatchedFilesParams>()
verify { mockWorkspaceService.didChangeWatchedFiles(capture(paramsSlot)) }
assertEquals(normalizeFileUri(newUri.toString()), paramsSlot.captured.changes[0].uri)
assertEquals(FileChangeType.Created, paramsSlot.captured.changes[0].type)
}

@Test
fun `test no invoked messages when events are empty`() = runTest {
// Act
Expand Down Expand Up @@ -510,20 +613,23 @@ class WorkspaceServiceHandlerTest {
assertEquals("folder2", paramsSlot.captured.event.removed[0].name)
}

private fun createMockVFileEvent(uri: URI, type: FileChangeType = FileChangeType.Changed, isDirectory: Boolean, extension: String = "py"): VFileEvent {
private fun createMockVirtualFile(uri: URI, fileName: String, isDirectory: Boolean = false): VirtualFile {
val nioPath = mockk<Path> {
every { toUri() } returns uri
}
val virtualFile = mockk<VirtualFile> {
return mockk<VirtualFile> {
every { [email protected] } returns isDirectory
every { toNioPath() } returns nioPath
every { url } returns uri.path
every { path } returns "${uri.path}.$extension"
every { path } returns "${uri.path}/$fileName"
every { fileSystem } returns mockk {
every { protocol } returns "file"
}
}
}

private fun createMockVFileEvent(uri: URI, type: FileChangeType = FileChangeType.Changed, isDirectory: Boolean = false, extension: String = "py"): VFileEvent {
val virtualFile = createMockVirtualFile(uri, "test.$extension", isDirectory)
return when (type) {
FileChangeType.Deleted -> mockk<VFileDeleteEvent>()
FileChangeType.Created -> mockk<VFileCreateEvent>()
Expand All @@ -533,49 +639,50 @@ class WorkspaceServiceHandlerTest {
}
}

// for didRename events
private fun createMockPropertyChangeEvent(
oldName: String,
newName: String,
isDirectory: Boolean = false,
): VFilePropertyChangeEvent {
val parentPath = mockk<Path>()
val filePath = mockk<Path>()
val oldUri = URI("file:///test/$oldName")
val newUri = URI("file:///test/$newName")
val file = createMockVirtualFile(newUri, newName, isDirectory)
every { file.parent } returns createMockVirtualFile(oldUri, oldName, isDirectory)

val parent = mockk<VirtualFile> {
every { toNioPath() } returns parentPath
every { [email protected] } returns isDirectory
every { path } returns "/test/$oldName"
every { url } returns "file:///test/$oldName"
every { fileSystem } returns mockk {
every { protocol } returns "file"
}
return mockk<VFilePropertyChangeEvent>().apply {
every { propertyName } returns VirtualFile.PROP_NAME
every { [email protected] } returns file
every { oldValue } returns oldName
every { newValue } returns newName
}
}

val file = mockk<VirtualFile> {
every { toNioPath() } returns filePath
every { [email protected] } returns parent
every { [email protected] } returns isDirectory
every { path } returns "/test/$newName"
every { url } returns "file:///test/$newName"
private fun createMockVFileMoveEvent(oldUri: URI, newUri: URI, fileName: String, isDirectory: Boolean = false): VFileMoveEvent {
val oldFile = createMockVirtualFile(oldUri, fileName, isDirectory)
val newFile = createMockVirtualFile(newUri, fileName, isDirectory)
return mockk<VFileMoveEvent>().apply {
every { file } returns newFile
every { oldPath } returns oldUri.path
every { oldParent } returns oldFile
}
}

private fun createMockVFileCopyEvent(originalUri: URI, newUri: URI, fileName: String): VFileCopyEvent {
val newParent = mockk<VirtualFile> {
every { findChild(any()) } returns createMockVirtualFile(newUri, fileName)
every { fileSystem } returns mockk {
every { protocol } returns "file"
}
}

every { parentPath.resolve(oldName) } returns mockk {
every { toUri() } returns URI("file:///test/$oldName")
}
every { filePath.toUri() } returns URI("file:///test/$newName")

return mockk<VFilePropertyChangeEvent>().apply {
every { propertyName } returns VirtualFile.PROP_NAME
every { [email protected] } returns file
every { oldValue } returns oldName
every { newValue } returns newName
return mockk<VFileCopyEvent>().apply {
every { file } returns createMockVirtualFile(originalUri, fileName)
every { [email protected] } returns newParent
every { newChildName } returns fileName
}
}



// for windows unit tests
private fun normalizeFileUri(uri: String): String {
if (!System.getProperty("os.name").lowercase().contains("windows")) {
Expand Down
Loading