From a4e01dab226987a2f05ad7ad8c93df4b07f24cdd Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Thu, 10 Jul 2025 09:58:44 -0700 Subject: [PATCH 1/7] add missing params to node --- .../services/amazonq/webview/BrowserConnector.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index 5ac079c8ac6..7067aee1abd 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -353,7 +353,20 @@ class BrowserConnector( } CHAT_INSERT_TO_CURSOR -> { - handleChat(AmazonQChatServer.insertToCursorPosition, node) + val editor = FileEditorManager.getInstance(project).selectedTextEditor + val textDocument = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) } + val cursorPosition = editor?.let { LspEditorUtil.getCursorState(it) } + + val enrichedParams = (node.params as? ObjectNode)?.apply { + put("textDocument", serializer.objectMapper.valueToTree(textDocument)) + put("cursorPosition", serializer.objectMapper.valueToTree(cursorPosition)) + } ?: node.params + + val enrichedNode = (node as ObjectNode).apply { + set("params", enrichedParams) + } + + handleChat(AmazonQChatServer.insertToCursorPosition, enrichedNode) } CHAT_LINK_CLICK -> { From 482523671c8395ff83b026b8d5ef668ed370dc66 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Thu, 10 Jul 2025 14:57:50 -0700 Subject: [PATCH 2/7] implement applyEdit client logic --- .../amazonq/webview/BrowserConnector.kt | 9 +-- .../amazonq/lsp/AmazonQLanguageClientImpl.kt | 18 +++++ .../amazonq/lsp/util/LspEditorUtil.kt | 75 ++++++++++++++++++- 3 files changed, 93 insertions(+), 9 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index 7067aee1abd..eabe822852f 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -355,13 +355,12 @@ class BrowserConnector( CHAT_INSERT_TO_CURSOR -> { val editor = FileEditorManager.getInstance(project).selectedTextEditor val textDocument = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) } - val cursorPosition = editor?.let { LspEditorUtil.getCursorState(it) } - + val cursorPosition = editor?.let { LspEditorUtil.getCursorPosition(it) } val enrichedParams = (node.params as? ObjectNode)?.apply { - put("textDocument", serializer.objectMapper.valueToTree(textDocument)) - put("cursorPosition", serializer.objectMapper.valueToTree(cursorPosition)) + set("cursorPosition", serializer.objectMapper.valueToTree(cursorPosition)) + set("textDocument", serializer.objectMapper.valueToTree(textDocument)) } ?: node.params - + val enrichedNode = (node as ObjectNode).apply { set("params", enrichedParams) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt index fe5f70bec8d..691da2354a0 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt @@ -21,6 +21,8 @@ import com.intellij.openapi.vfs.VfsUtil import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFileManager import migration.software.aws.toolkits.jetbrains.settings.AwsSettings +import org.eclipse.lsp4j.ApplyWorkspaceEditParams +import org.eclipse.lsp4j.ApplyWorkspaceEditResponse import org.eclipse.lsp4j.ConfigurationParams import org.eclipse.lsp4j.MessageActionItem import org.eclipse.lsp4j.MessageParams @@ -62,6 +64,7 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowO import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ShowSaveFileDialogResult import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentials.ConnectionMetadata +import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.LspEditorUtil import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.TelemetryParsingUtil import software.aws.toolkits.jetbrains.services.codewhisperer.customization.CodeWhispererModelConfigurator import software.aws.toolkits.jetbrains.services.telemetry.TelemetryService @@ -567,6 +570,21 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC ) } + override fun applyEdit(params: ApplyWorkspaceEditParams): CompletableFuture { + return CompletableFuture.supplyAsync( + { + try { + LspEditorUtil.applyWorkspaceEdit(project, params.edit) + ApplyWorkspaceEditResponse(true) + } catch (e: Exception) { + LOG.warn(e) { "Failed to apply workspace edit" } + ApplyWorkspaceEditResponse(false) + } + }, + ApplicationManager.getApplication()::invokeLater + ) + } + private fun refreshVfs(path: String) { val currPath = Paths.get(path) if (currPath.startsWith(localHistoryPath)) return diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt index e7ece95f539..af580fab2bb 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt @@ -4,11 +4,18 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.util import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VfsUtilCore import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.WorkspaceEdit import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CursorPosition @@ -63,13 +70,73 @@ object LspEditorUtil { ) } else { return@runReadAction CursorPosition( - Position( - editor.caretModel.primaryCaret.logicalPosition.line, - editor.caretModel.primaryCaret.logicalPosition.column - ) + getCursorPosition(editor) ) } } + fun getCursorPosition(editor: Editor): Position = + runReadAction { + Position( + editor.caretModel.primaryCaret.logicalPosition.line, + editor.caretModel.primaryCaret.logicalPosition.column + ) + } + + fun applyWorkspaceEdit(project: Project, edit: WorkspaceEdit) { + WriteCommandAction.runWriteCommandAction(project) { + edit.documentChanges?.forEach { change -> + if (change.isLeft) { + val textDocumentEdit = change.left + val file = VirtualFileManager.getInstance().findFileByUrl(textDocumentEdit.textDocument.uri) + file?.let { + val document = FileDocumentManager.getInstance().getDocument(it) + val editor = FileEditorManager.getInstance(project).getSelectedEditor(it)?.let { fileEditor -> + if (fileEditor is com.intellij.openapi.fileEditor.TextEditor) fileEditor.editor else null + } + document?.let { doc -> + textDocumentEdit.edits.forEach { textEdit -> + val startOffset = if (editor != null) { + editor.logicalPositionToOffset(LogicalPosition(textEdit.range.start.line, textEdit.range.start.character)) + } else { + doc.getLineStartOffset(textEdit.range.start.line) + textEdit.range.start.character + } + val endOffset = if (editor != null) { + editor.logicalPositionToOffset(LogicalPosition(textEdit.range.end.line, textEdit.range.end.character)) + } else { + doc.getLineStartOffset(textEdit.range.end.line) + textEdit.range.end.character + } + doc.replaceString(startOffset, endOffset, textEdit.newText) + } + } + } + } + } ?: edit.changes?.forEach { (uri, textEdits) -> + val file = VirtualFileManager.getInstance().findFileByUrl(uri) + file?.let { + val document = FileDocumentManager.getInstance().getDocument(it) + val editor = FileEditorManager.getInstance(project).getSelectedEditor(it)?.let { fileEditor -> + if (fileEditor is com.intellij.openapi.fileEditor.TextEditor) fileEditor.editor else null + } + document?.let { doc -> + textEdits.forEach { textEdit -> + val startOffset = if (editor != null) { + editor.logicalPositionToOffset(LogicalPosition(textEdit.range.start.line, textEdit.range.start.character)) + } else { + doc.getLineStartOffset(textEdit.range.start.line) + textEdit.range.start.character + } + val endOffset = if (editor != null) { + editor.logicalPositionToOffset(LogicalPosition(textEdit.range.end.line, textEdit.range.end.character)) + } else { + doc.getLineStartOffset(textEdit.range.end.line) + textEdit.range.end.character + } + doc.replaceString(startOffset, endOffset, textEdit.newText) + } + } + } + } + } + } + private val LOG = getLogger() } From 18d754fc9a124d8f880e791edc924ce8dc62521a Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Thu, 10 Jul 2025 15:10:10 -0700 Subject: [PATCH 3/7] clean up util functions --- .../amazonq/webview/BrowserConnector.kt | 1 - .../amazonq/lsp/AmazonQLanguageClientImpl.kt | 5 +- .../amazonq/lsp/util/LspEditorUtil.kt | 73 +++++++------------ 3 files changed, 30 insertions(+), 49 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index eabe822852f..1f42b07c2ee 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -364,7 +364,6 @@ class BrowserConnector( val enrichedNode = (node as ObjectNode).apply { set("params", enrichedParams) } - handleChat(AmazonQChatServer.insertToCursorPosition, enrichedNode) } diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt index 691da2354a0..4b105142484 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLanguageClientImpl.kt @@ -570,8 +570,8 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC ) } - override fun applyEdit(params: ApplyWorkspaceEditParams): CompletableFuture { - return CompletableFuture.supplyAsync( + override fun applyEdit(params: ApplyWorkspaceEditParams): CompletableFuture = + CompletableFuture.supplyAsync( { try { LspEditorUtil.applyWorkspaceEdit(project, params.edit) @@ -583,7 +583,6 @@ class AmazonQLanguageClientImpl(private val project: Project) : AmazonQLanguageC }, ApplicationManager.getApplication()::invokeLater ) - } private fun refreshVfs(path: String) { val currPath = Paths.get(path) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt index af580fab2bb..9a49884d177 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt @@ -5,6 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.util import com.intellij.openapi.application.runReadAction import com.intellij.openapi.command.WriteCommandAction +import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.LogicalPosition import com.intellij.openapi.fileEditor.FileDocumentManager @@ -15,6 +16,7 @@ import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.VirtualFileManager import org.eclipse.lsp4j.Position import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextEdit import org.eclipse.lsp4j.WorkspaceEdit import software.aws.toolkits.core.utils.getLogger import software.aws.toolkits.core.utils.warn @@ -88,55 +90,36 @@ object LspEditorUtil { edit.documentChanges?.forEach { change -> if (change.isLeft) { val textDocumentEdit = change.left - val file = VirtualFileManager.getInstance().findFileByUrl(textDocumentEdit.textDocument.uri) - file?.let { - val document = FileDocumentManager.getInstance().getDocument(it) - val editor = FileEditorManager.getInstance(project).getSelectedEditor(it)?.let { fileEditor -> - if (fileEditor is com.intellij.openapi.fileEditor.TextEditor) fileEditor.editor else null - } - document?.let { doc -> - textDocumentEdit.edits.forEach { textEdit -> - val startOffset = if (editor != null) { - editor.logicalPositionToOffset(LogicalPosition(textEdit.range.start.line, textEdit.range.start.character)) - } else { - doc.getLineStartOffset(textEdit.range.start.line) + textEdit.range.start.character - } - val endOffset = if (editor != null) { - editor.logicalPositionToOffset(LogicalPosition(textEdit.range.end.line, textEdit.range.end.character)) - } else { - doc.getLineStartOffset(textEdit.range.end.line) + textEdit.range.end.character - } - doc.replaceString(startOffset, endOffset, textEdit.newText) - } - } - } - } - } ?: edit.changes?.forEach { (uri, textEdits) -> - val file = VirtualFileManager.getInstance().findFileByUrl(uri) - file?.let { - val document = FileDocumentManager.getInstance().getDocument(it) - val editor = FileEditorManager.getInstance(project).getSelectedEditor(it)?.let { fileEditor -> - if (fileEditor is com.intellij.openapi.fileEditor.TextEditor) fileEditor.editor else null - } - document?.let { doc -> - textEdits.forEach { textEdit -> - val startOffset = if (editor != null) { - editor.logicalPositionToOffset(LogicalPosition(textEdit.range.start.line, textEdit.range.start.character)) - } else { - doc.getLineStartOffset(textEdit.range.start.line) + textEdit.range.start.character - } - val endOffset = if (editor != null) { - editor.logicalPositionToOffset(LogicalPosition(textEdit.range.end.line, textEdit.range.end.character)) - } else { - doc.getLineStartOffset(textEdit.range.end.line) + textEdit.range.end.character - } - doc.replaceString(startOffset, endOffset, textEdit.newText) - } - } + applyEditsToFile(project, textDocumentEdit.textDocument.uri, textDocumentEdit.edits) } } + + edit.changes?.forEach { (uri, textEdits) -> + applyEditsToFile(project, uri, textEdits) + } } } + private fun applyEditsToFile(project: Project, uri: String, textEdits: List) { + val file = VirtualFileManager.getInstance().findFileByUrl(uri) ?: return + val document = FileDocumentManager.getInstance().getDocument(file) ?: return + val editor = FileEditorManager.getInstance(project).getSelectedEditor(file)?.let { + if (it is com.intellij.openapi.fileEditor.TextEditor) it.editor else null + } + + textEdits.forEach { textEdit -> + val startOffset = calculateOffset(editor, document, textEdit.range.start) + val endOffset = calculateOffset(editor, document, textEdit.range.end) + document.replaceString(startOffset, endOffset, textEdit.newText) + } + } + + private fun calculateOffset(editor: Editor?, document: Document, position: Position): Int = + if (editor != null) { + editor.logicalPositionToOffset(LogicalPosition(position.line, position.character)) + } else { + document.getLineStartOffset(position.line) + position.character + } + private val LOG = getLogger() } From e59852e0f522e28d46584ebda8dbc192f21321e7 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Thu, 10 Jul 2025 15:56:37 -0700 Subject: [PATCH 4/7] changelog --- .../bugfix-d5bbb805-aa36-4e2b-9bc2-7377c184c716.json | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changes/next-release/bugfix-d5bbb805-aa36-4e2b-9bc2-7377c184c716.json diff --git a/.changes/next-release/bugfix-d5bbb805-aa36-4e2b-9bc2-7377c184c716.json b/.changes/next-release/bugfix-d5bbb805-aa36-4e2b-9bc2-7377c184c716.json new file mode 100644 index 00000000000..6bfec087f3b --- /dev/null +++ b/.changes/next-release/bugfix-d5bbb805-aa36-4e2b-9bc2-7377c184c716.json @@ -0,0 +1,4 @@ +{ + "type" : "bugfix", + "description" : "- Fixed \"Insert to Cursor\" button to correctly insert code blocks at the current cursor position in the active file" +} \ No newline at end of file From 2d5914342b8c5da4530a0cb1fcce857d71063a75 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Tue, 15 Jul 2025 10:57:13 -0700 Subject: [PATCH 5/7] reduce serializer calls --- .../services/amazonq/webview/BrowserConnector.kt | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt index 1f42b07c2ee..a42601ea577 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/BrowserConnector.kt @@ -354,16 +354,20 @@ class BrowserConnector( CHAT_INSERT_TO_CURSOR -> { val editor = FileEditorManager.getInstance(project).selectedTextEditor - val textDocument = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) } + val textDocumentIdentifier = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) } val cursorPosition = editor?.let { LspEditorUtil.getCursorPosition(it) } - val enrichedParams = (node.params as? ObjectNode)?.apply { - set("cursorPosition", serializer.objectMapper.valueToTree(cursorPosition)) - set("textDocument", serializer.objectMapper.valueToTree(textDocument)) - } ?: node.params + val enrichmentParams = mapOf( + "textDocument" to textDocumentIdentifier, + "cursorPosition" to cursorPosition, + ) + + val insertToCursorPositionParams: ObjectNode = (node.params as ObjectNode) + .setAll(serializer.objectMapper.valueToTree(enrichmentParams)) val enrichedNode = (node as ObjectNode).apply { - set("params", enrichedParams) + set("params", insertToCursorPositionParams) } + handleChat(AmazonQChatServer.insertToCursorPosition, enrichedNode) } From 753ac308ae70f9bdc9c8036f424962e18156f8b7 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Tue, 15 Jul 2025 15:35:42 -0700 Subject: [PATCH 6/7] tests for applyWorkspaceEdit --- .../lsp/util/ApplyWorkspaceEditTest.kt | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt new file mode 100644 index 00000000000..28698c16967 --- /dev/null +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt @@ -0,0 +1,213 @@ +// 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.command.WriteCommandAction +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.LogicalPosition +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditorManager +import com.intellij.openapi.fileEditor.TextEditor +import com.intellij.openapi.project.Project +import com.intellij.openapi.vfs.VirtualFile +import com.intellij.openapi.vfs.VirtualFileManager +import com.intellij.testFramework.ApplicationExtension +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.slot +import io.mockk.verify +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import org.eclipse.lsp4j.TextDocumentEdit +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j.VersionedTextDocumentIdentifier +import org.eclipse.lsp4j.WorkspaceEdit +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(ApplicationExtension::class) +class ApplyWorkspaceEditTest { + + private lateinit var project: Project + private lateinit var virtualFileManager: VirtualFileManager + private lateinit var fileDocumentManager: FileDocumentManager + private lateinit var fileEditorManager: FileEditorManager + + @BeforeEach + fun setUp() { + virtualFileManager = mockk(relaxed = true) + fileDocumentManager = mockk(relaxed = true) + fileEditorManager = mockk(relaxed = true) + project = mockk(relaxed = true) + + mockkStatic(WriteCommandAction::class) + every { WriteCommandAction.runWriteCommandAction(any(), any()) } answers { + secondArg().run() + } + + mockkStatic(VirtualFileManager::getInstance) + every { VirtualFileManager.getInstance() } returns virtualFileManager + + mockkStatic(FileDocumentManager::getInstance) + every { FileDocumentManager.getInstance() } returns fileDocumentManager + + mockkStatic(FileEditorManager::getInstance) + every { FileEditorManager.getInstance(any()) } returns fileEditorManager + } + + @Test + fun `test applyWorkspaceEdit with changes`() { + val uri = "file:///test.kt" + val document = mockk(relaxed = true) + val file = mockk() + val textEdit = TextEdit(Range(Position(0, 0), Position(0, 5)), "newText") + + every { virtualFileManager.findFileByUrl(uri) } returns file + every { fileDocumentManager.getDocument(file) } returns document + every { document.getLineStartOffset(0) } returns 0 + + val workspaceEdit = WorkspaceEdit().apply { + changes = mapOf(uri to listOf(textEdit)) + } + + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + + verify { document.replaceString(0, 5, "newText") } + } + + @Test + fun `test applyWorkspaceEdit with documentChanges`() { + val uri = "file:///test.kt" + val document = mockk(relaxed = true) + val file = mockk() + val textEdit = TextEdit(Range(Position(0, 0), Position(0, 5)), "newText") + + every { virtualFileManager.findFileByUrl(uri) } returns file + every { fileDocumentManager.getDocument(file) } returns document + every { document.getLineStartOffset(0) } returns 0 + + val versionedIdentifier = VersionedTextDocumentIdentifier(uri, 1) + val textDocumentEdit = TextDocumentEdit(versionedIdentifier, listOf(textEdit)) + val workspaceEdit = WorkspaceEdit().apply { + documentChanges = listOf(Either.forLeft(textDocumentEdit)) + } + + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + + verify { document.replaceString(0, 5, "newText") } + } + + @Test + fun `test applyWorkspaceEdit with editor`() { + val uri = "file:///test.kt" + val document = mockk(relaxed = true) + val file = mockk() + val editor = mockk() + val textEditor = mockk() + + every { virtualFileManager.findFileByUrl(uri) } returns file + every { fileDocumentManager.getDocument(file) } returns document + every { fileEditorManager.getSelectedEditor(file) } returns textEditor + every { textEditor.editor } returns editor + + val positionSlot = slot() + every { editor.logicalPositionToOffset(capture(positionSlot)) } answers { + if (positionSlot.captured.line == 0 && positionSlot.captured.column == 0) 0 else 5 + } + + val textEdit = TextEdit(Range(Position(0, 0), Position(0, 5)), "newText") + val workspaceEdit = WorkspaceEdit().apply { + changes = mapOf(uri to listOf(textEdit)) + } + + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + + verify { document.replaceString(0, 5, "newText") } + } + + @Test + fun `test applyWorkspaceEdit with both changes and documentChanges`() { + val uri1 = "file:///test1.kt" + val uri2 = "file:///test2.kt" + val document1 = mockk(relaxed = true) + val document2 = mockk(relaxed = true) + val file1 = mockk() + val file2 = mockk() + + val textEdit1 = TextEdit(Range(Position(0, 0), Position(0, 5)), "newText1") + val textEdit2 = TextEdit(Range(Position(1, 0), Position(1, 10)), "newText2") + + every { virtualFileManager.findFileByUrl(uri1) } returns file1 + every { virtualFileManager.findFileByUrl(uri2) } returns file2 + every { fileDocumentManager.getDocument(file1) } returns document1 + every { fileDocumentManager.getDocument(file2) } returns document2 + every { document1.getLineStartOffset(0) } returns 0 + every { document2.getLineStartOffset(1) } returns 100 + + val versionedIdentifier = VersionedTextDocumentIdentifier(uri1, 1) + val textDocumentEdit = TextDocumentEdit(versionedIdentifier, listOf(textEdit1)) + + val workspaceEdit = WorkspaceEdit().apply { + documentChanges = listOf(Either.forLeft(textDocumentEdit)) + changes = mapOf(uri2 to listOf(textEdit2)) + } + + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + + verify { document1.replaceString(0, 5, "newText1") } + verify { document2.replaceString(100, 110, "newText2") } + } + + @Test + fun `test applyWorkspaceEdit with multiple edits to same file`() { + val uri = "file:///test.kt" + val document = mockk(relaxed = true) + val file = mockk() + + val textEdit1 = TextEdit(Range(Position(0, 0), Position(0, 5)), "newText1") + val textEdit2 = TextEdit(Range(Position(1, 0), Position(1, 10)), "newText2") + + every { virtualFileManager.findFileByUrl(uri) } returns file + every { fileDocumentManager.getDocument(file) } returns document + every { document.getLineStartOffset(0) } returns 0 + every { document.getLineStartOffset(1) } returns 100 + + val workspaceEdit = WorkspaceEdit().apply { + changes = mapOf(uri to listOf(textEdit1, textEdit2)) + } + + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + + verify { document.replaceString(0, 5, "newText1") } + verify { document.replaceString(100, 110, "newText2") } + } + + @Test + fun `test applyWorkspaceEdit with empty edits`() { + val workspaceEdit = WorkspaceEdit() + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + + // No verification needed - just ensuring no exceptions + } + + + @Test + fun `test applyWorkspaceEdit with invalid file`() { + val uri = "file:///nonexistent.kt" + val textEdit = TextEdit(Range(Position(0, 0), Position(0, 5)), "newText") + + every { virtualFileManager.findFileByUrl(uri) } returns null + + val workspaceEdit = WorkspaceEdit().apply { + changes = mapOf(uri to listOf(textEdit)) + } + + // Execute - should not throw exception + LspEditorUtil.applyWorkspaceEdit(project, workspaceEdit) + } +} From 3f983ac61d153aeb4733a7d1ba7f5c421a88e733 Mon Sep 17 00:00:00 2001 From: samgst-amazon Date: Tue, 15 Jul 2025 15:42:44 -0700 Subject: [PATCH 7/7] formatting --- .../jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt | 6 +----- .../services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt | 1 - 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt index 9a49884d177..d7b80ebd54d 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/LspEditorUtil.kt @@ -115,11 +115,7 @@ object LspEditorUtil { } private fun calculateOffset(editor: Editor?, document: Document, position: Position): Int = - if (editor != null) { - editor.logicalPositionToOffset(LogicalPosition(position.line, position.character)) - } else { - document.getLineStartOffset(position.line) + position.character - } + editor?.logicalPositionToOffset(LogicalPosition(position.line, position.character)) ?: (document.getLineStartOffset(position.line) + position.character) private val LOG = getLogger() } diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt index 28698c16967..1de4c0ba0ce 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/util/ApplyWorkspaceEditTest.kt @@ -195,7 +195,6 @@ class ApplyWorkspaceEditTest { // No verification needed - just ensuring no exceptions } - @Test fun `test applyWorkspaceEdit with invalid file`() { val uri = "file:///nonexistent.kt"