From 30f3ccac772d165295d54e2d5064b8caed76966c Mon Sep 17 00:00:00 2001 From: Richard Li <742829+rli@users.noreply.github.com> Date: Fri, 9 May 2025 13:08:02 -0700 Subject: [PATCH 1/4] fix(amazonq): fix UI freeze when editing large files (#5685) --- ...-1d632e06-24fa-40f1-a8ae-33cca0d86ec4.json | 4 + .../TextDocumentServiceHandler.kt | 78 ++++++--- .../TextDocumentServiceHandlerTest.kt | 148 ++++++++---------- 3 files changed, 129 insertions(+), 101 deletions(-) create mode 100644 .changes/next-release/bugfix-1d632e06-24fa-40f1-a8ae-33cca0d86ec4.json diff --git a/.changes/next-release/bugfix-1d632e06-24fa-40f1-a8ae-33cca0d86ec4.json b/.changes/next-release/bugfix-1d632e06-24fa-40f1-a8ae-33cca0d86ec4.json new file mode 100644 index 00000000000..b33530733ee --- /dev/null +++ b/.changes/next-release/bugfix-1d632e06-24fa-40f1-a8ae-33cca0d86ec4.json @@ -0,0 +1,4 @@ +{ + "type" : "bugfix", + "description" : "Fix UI freezes that may occur when interacting with large files in the editor" +} \ No newline at end of file diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt index bbf60200810..1fd35889772 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt @@ -4,7 +4,10 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument import com.intellij.openapi.Disposable +import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.event.DocumentEvent +import com.intellij.openapi.editor.event.DocumentListener import com.intellij.openapi.fileEditor.FileDocumentManager import com.intellij.openapi.fileEditor.FileDocumentManagerListener import com.intellij.openapi.fileEditor.FileEditorManager @@ -29,10 +32,11 @@ import software.aws.toolkits.jetbrains.utils.pluginAwareExecuteOnPooledThread class TextDocumentServiceHandler( private val project: Project, - serverInstance: Disposable, + private val serverInstance: Disposable, ) : FileDocumentManagerListener, FileEditorManagerListener, - BulkFileListener { + BulkFileListener, + DocumentListener { init { // didOpen & didClose events @@ -61,18 +65,30 @@ class TextDocumentServiceHandler( } private fun handleFileOpened(file: VirtualFile) { + ApplicationManager.getApplication().runReadAction { + FileDocumentManager.getInstance().getDocument(file)?.addDocumentListener( + object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + realTimeEdit(event) + } + }, + serverInstance + ) + } AmazonQLspService.executeIfRunning(project) { languageServer -> toUriString(file)?.let { uri -> - languageServer.textDocumentService.didOpen( - DidOpenTextDocumentParams().apply { - textDocument = TextDocumentItem().apply { - this.uri = uri - text = file.inputStream.readAllBytes().decodeToString() - languageId = file.fileType.name.lowercase() - version = file.modificationStamp.toInt() + pluginAwareExecuteOnPooledThread { + languageServer.textDocumentService.didOpen( + DidOpenTextDocumentParams().apply { + textDocument = TextDocumentItem().apply { + this.uri = uri + text = file.inputStream.readAllBytes().decodeToString() + languageId = file.fileType.name.lowercase() + version = file.modificationStamp.toInt() + } } - } - ) + ) + } } } } @@ -81,14 +97,16 @@ class TextDocumentServiceHandler( AmazonQLspService.executeIfRunning(project) { languageServer -> val file = FileDocumentManager.getInstance().getFile(document) ?: return@executeIfRunning toUriString(file)?.let { uri -> - languageServer.textDocumentService.didSave( - DidSaveTextDocumentParams().apply { - textDocument = TextDocumentIdentifier().apply { - this.uri = uri + pluginAwareExecuteOnPooledThread { + languageServer.textDocumentService.didSave( + DidSaveTextDocumentParams().apply { + textDocument = TextDocumentIdentifier().apply { + this.uri = uri + } + text = document.text } - text = document.text - } - ) + ) + } } } } @@ -141,4 +159,28 @@ class TextDocumentServiceHandler( } } } + + private fun realTimeEdit(event: DocumentEvent) { + AmazonQLspService.executeIfRunning(project) { languageServer -> + pluginAwareExecuteOnPooledThread { + val vFile = FileDocumentManager.getInstance().getFile(event.document) ?: return@pluginAwareExecuteOnPooledThread + toUriString(vFile)?.let { uri -> + languageServer.textDocumentService.didChange( + DidChangeTextDocumentParams().apply { + textDocument = VersionedTextDocumentIdentifier().apply { + this.uri = uri + version = event.document.modificationStamp.toInt() + } + contentChanges = listOf( + TextDocumentContentChangeEvent().apply { + text = event.document.text + } + ) + } + ) + } + } + } + // Process document changes here + } } diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt index 96da1fc4318..0ee0d61106c 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt @@ -3,29 +3,26 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument -import com.intellij.openapi.Disposable -import com.intellij.openapi.application.Application -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.components.serviceIfCreated +import com.intellij.openapi.application.writeAction import com.intellij.openapi.editor.Document import com.intellij.openapi.fileEditor.FileDocumentManager -import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileTypes.FileType -import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent import com.intellij.openapi.vfs.newvfs.events.VFileEvent -import com.intellij.util.messages.MessageBus -import com.intellij.util.messages.MessageBusConnection +import com.intellij.openapi.vfs.writeText +import com.intellij.testFramework.DisposableRule +import com.intellij.testFramework.fixtures.CodeInsightTestFixture +import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory +import com.intellij.testFramework.replaceService import io.mockk.every -import io.mockk.just import io.mockk.mockk import io.mockk.mockkObject import io.mockk.mockkStatic -import io.mockk.runs import io.mockk.slot import io.mockk.verify import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.withContext import org.assertj.core.api.Assertions.assertThat import org.eclipse.lsp4j.DidChangeTextDocumentParams import org.eclipse.lsp4j.DidCloseTextDocumentParams @@ -34,42 +31,51 @@ import org.eclipse.lsp4j.DidSaveTextDocumentParams import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage import org.eclipse.lsp4j.services.TextDocumentService import org.junit.Before +import org.junit.Rule import org.junit.Test +import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLanguageServer import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.amazonq.lsp.util.FileUriUtil +import software.aws.toolkits.jetbrains.utils.rules.CodeInsightTestFixtureRule +import software.aws.toolkits.jetbrains.utils.satisfiesKt import java.net.URI import java.nio.file.Path -import java.util.concurrent.Callable import java.util.concurrent.CompletableFuture +import kotlin.collections.first class TextDocumentServiceHandlerTest { - private lateinit var project: Project - private lateinit var mockFileEditorManager: FileEditorManager private lateinit var mockLanguageServer: AmazonQLanguageServer private lateinit var mockTextDocumentService: TextDocumentService private lateinit var sut: TextDocumentServiceHandler - private lateinit var mockApplication: Application + + @get:Rule + val projectRule = object : CodeInsightTestFixtureRule() { + override fun createTestFixture(): CodeInsightTestFixture { + val fixtureFactory = IdeaTestFixtureFactory.getFixtureFactory() + val fixtureBuilder = fixtureFactory.createLightFixtureBuilder(testDescription, testName) + val newFixture = fixtureFactory + .createCodeInsightFixture(fixtureBuilder.fixture, fixtureFactory.createTempDirTestFixture()) + newFixture.setUp() + newFixture.testDataPath = testDataPath + + return newFixture + } + } + + @get:Rule + val disposableRule = DisposableRule() @Before fun setup() { - project = mockk() mockTextDocumentService = mockk() mockLanguageServer = mockk() - mockApplication = mockk() - mockkStatic(ApplicationManager::class) - every { ApplicationManager.getApplication() } returns mockApplication - every { mockApplication.executeOnPooledThread(any>()) } answers { - CompletableFuture.completedFuture(firstArg>().call()) - } - // Mock the LSP service - val mockLspService = mockk() + val mockLspService = mockk(relaxed = true) // Mock the service methods on Project - every { project.getService(AmazonQLspService::class.java) } returns mockLspService - every { project.serviceIfCreated() } returns mockLspService + projectRule.project.replaceService(AmazonQLspService::class.java, mockLspService, disposableRule.disposable) // Mock the LSP service's executeSync method as a suspend function every { @@ -86,19 +92,7 @@ class TextDocumentServiceHandlerTest { every { mockTextDocumentService.didOpen(any()) } returns Unit every { mockTextDocumentService.didClose(any()) } returns Unit - // Mock message bus - val messageBus = mockk() - every { project.messageBus } returns messageBus - val mockConnection = mockk() - every { messageBus.connect(any()) } returns mockConnection - every { mockConnection.subscribe(any(), any()) } just runs - - // Mock FileEditorManager - mockFileEditorManager = mockk() - every { mockFileEditorManager.openFiles } returns emptyArray() - every { project.getService(FileEditorManager::class.java) } returns mockFileEditorManager - - sut = TextDocumentServiceHandler(project, mockk()) + sut = TextDocumentServiceHandler(projectRule.project, mockk()) } @Test @@ -136,41 +130,39 @@ class TextDocumentServiceHandlerTest { @Test fun `didOpen runs on service init`() = runTest { - val uri = URI.create("file:///test/path/file.txt") val content = "test content" - val file = createMockVirtualFile(uri, content) - - every { mockFileEditorManager.openFiles } returns arrayOf(file) + val file = withContext(EDT) { + projectRule.fixture.createFile("name", content).also { projectRule.fixture.openFileInEditor(it) } + } - sut = TextDocumentServiceHandler(project, mockk()) + sut = TextDocumentServiceHandler(projectRule.project, mockk()) - val paramsSlot = slot() + val paramsSlot = mutableListOf() verify { mockTextDocumentService.didOpen(capture(paramsSlot)) } - with(paramsSlot.captured.textDocument) { - assertThat(this.uri).isEqualTo(normalizeFileUri(uri.toString())) - assertThat(text).isEqualTo(content) - assertThat(languageId).isEqualTo("java") - assertThat(version).isEqualTo(1) + assertThat(paramsSlot.first().textDocument).satisfiesKt { + assertThat(it.uri).isEqualTo(file.toNioPath().toUri().toString()) + assertThat(it.text).isEqualTo(content) + assertThat(it.languageId).isEqualTo("plain_text") } } @Test fun `didOpen runs on fileOpened`() = runTest { - val uri = URI.create("file:///test/path/file.txt") val content = "test content" - val file = createMockVirtualFile(uri, content) + val file = withContext(EDT) { + projectRule.fixture.createFile("name", content).also { projectRule.fixture.openFileInEditor(it) } + } sut.fileOpened(mockk(), file) - val paramsSlot = slot() + val paramsSlot = mutableListOf() verify { mockTextDocumentService.didOpen(capture(paramsSlot)) } - with(paramsSlot.captured.textDocument) { - assertThat(this.uri).isEqualTo(normalizeFileUri(uri.toString())) - assertThat(text).isEqualTo(content) - assertThat(languageId).isEqualTo("java") - assertThat(version).isEqualTo(1) + assertThat(paramsSlot.first().textDocument).satisfiesKt { + assertThat(it.uri).isEqualTo(file.toNioPath().toUri().toString()) + assertThat(it.text).isEqualTo(content) + assertThat(it.languageId).isEqualTo("plain_text") } } @@ -189,38 +181,23 @@ class TextDocumentServiceHandlerTest { @Test fun `didChange runs on content change events`() = runTest { - val uri = URI.create("file:///test/path/file.txt") - val document = mockk { - every { text } returns "changed content" - every { modificationStamp } returns 123L - } - - val file = createMockVirtualFile(uri) - - val changeEvent = mockk { - every { this@mockk.file } returns file - } - - // Mock FileDocumentManager - val fileDocumentManager = mockk { - every { getCachedDocument(file) } returns document - } + val file = withContext(EDT) { + projectRule.fixture.createFile("name", "").also { + projectRule.fixture.openFileInEditor(it) - mockkStatic(FileDocumentManager::class) { - every { FileDocumentManager.getInstance() } returns fileDocumentManager - - // Call the handler method - sut.after(mutableListOf(changeEvent)) + writeAction { + it.writeText("changed content") + } + } } // Verify the correct LSP method was called with matching parameters - val paramsSlot = slot() + val paramsSlot = mutableListOf() verify { mockTextDocumentService.didChange(capture(paramsSlot)) } - with(paramsSlot.captured) { - assertThat(textDocument.uri).isEqualTo(normalizeFileUri(uri.toString())) - assertThat(textDocument.version).isEqualTo(123) - assertThat(contentChanges[0].text).isEqualTo("changed content") + assertThat(paramsSlot.first()).satisfiesKt { + assertThat(it.textDocument.uri).isEqualTo(file.toNioPath().toUri().toString()) + assertThat(it.contentChanges[0].text).isEqualTo("changed content") } } @@ -335,6 +312,11 @@ class TextDocumentServiceHandlerTest { return uri } + if (uri.startsWith("file://C:/")) { + val path = uri.substringAfter("file://C:/") + return "file:///C:/$path" + } + val path = uri.substringAfter("file:///") return "file:///C:/$path" } From f72b7db814b35361bd59c5af8b67bb32ae09a770 Mon Sep 17 00:00:00 2001 From: manodnyab <66754471+manodnyab@users.noreply.github.com> Date: Fri, 9 May 2025 13:08:59 -0700 Subject: [PATCH 2/4] fix(amazonq): Prevent error messages from being shown on stop code generation (#5703) --- .../amazonq/webview/BrowserConnector.kt | 21 +++---------------- 1 file changed, 3 insertions(+), 18 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 8db334596df..4d7b3a34d4e 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 @@ -12,6 +12,7 @@ import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.options.ShowSettingsUtil import com.intellij.openapi.project.Project import com.intellij.ui.jcef.JBCefJSQuery.Response +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope @@ -65,7 +66,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatN import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatPrompt import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatReadyNotification -import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ChatUiMessageParams import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.ConversationClickRequest import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyCodeToClipboardNotification import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.chat.CopyCodeToClipboardParams @@ -114,7 +114,6 @@ import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.AmazonQThe import software.aws.toolkits.jetbrains.services.amazonq.webview.theme.ThemeBrowserAdapter import software.aws.toolkits.jetbrains.services.codewhisperer.settings.CodeWhispererConfigurable import software.aws.toolkits.jetbrains.settings.MeetQSettings -import software.aws.toolkits.resources.AwsCoreBundle import software.aws.toolkits.telemetry.MetricResult import software.aws.toolkits.telemetry.Telemetry import java.util.concurrent.CompletableFuture @@ -466,22 +465,6 @@ class BrowserConnector( } cancelInflightRequests(stopResponseRequest.params.tabId) chatCommunicationManager.removePartialChatMessage(stopResponseRequest.params.tabId) - - val paramsJson = Gson().toJson( - // https://github.com/aws/language-servers/blob/1c0d88806087125b6fc561f610cc15e98127c6bf/server/aws-lsp-codewhisperer/src/language-server/agenticChat/agenticChatController.ts#L403 - ChatUiMessageParams( - title = AwsCoreBundle.message("amazonqChat.stopChatResponse"), - body = "" - ) - ) - - val uiMessage = ChatCommunicationManager.convertToJsonToSendToChat( - command = SEND_CHAT_COMMAND_PROMPT, - tabId = stopResponseRequest.params.tabId, - params = paramsJson.toString(), - isPartialResult = false - ) - browser.postChat(uiMessage) } OPEN_SETTINGS -> { val openSettingsNotification = serializer.deserializeChatMessages(node) @@ -514,6 +497,8 @@ class BrowserConnector( ) browser.postChat(messageToChat) chatCommunicationManager.removeInflightRequestForTab(tabId) + } catch (e: CancellationException) { + LOG.warn { "Cancelled chat generation" } } catch (e: Exception) { LOG.error { "Failed to send chat message $e" } browser.postChat(chatCommunicationManager.getErrorUiMessage(tabId, e, partialResultToken)) From 6276aa155e5f7b0e94ffef6d0e7411295ec4aa21 Mon Sep 17 00:00:00 2001 From: Richard Li <742829+rli@users.noreply.github.com> Date: Fri, 9 May 2025 13:12:48 -0700 Subject: [PATCH 3/4] fix(amazonq): fix chat refresh button (#5701) Previously, the browser waits for the server initialization to succeed before unlocking the loading process. When reloading the webview, since the server has already been initialized, it does not receive an initialization event and is stuck loading forever. Fix by moving to a hot flow that always returns the latest instance of AmazonQLanguageServer. While this could be any value, the idea is to bring us closer to the state where we can tie the lifetime of ChatCommunicationManager to the webview instance so that we can delete AsyncChatUiListener. Additionally this should also address the rare race condition where the server finishes startup before the webview finishes drawing. --- .../amazonq/toolwindow/AmazonQPanel.kt | 21 +++++++++++------- .../services/amazonq/webview/Browser.kt | 22 +++++++++---------- .../services/amazonq/lsp/AmazonQLspService.kt | 15 ++++++++----- 3 files changed, 32 insertions(+), 26 deletions(-) diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt index 92e81d376b1..2ff79e68005 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/toolwindow/AmazonQPanel.kt @@ -5,8 +5,6 @@ package software.aws.toolkits.jetbrains.services.amazonq.toolwindow import com.intellij.idea.AppMode import com.intellij.openapi.Disposable -import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer @@ -19,12 +17,15 @@ import com.intellij.ui.dsl.builder.AlignY import com.intellij.ui.dsl.builder.panel import com.intellij.ui.jcef.JBCefApp import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import software.aws.toolkits.jetbrains.core.coroutines.EDT import software.aws.toolkits.jetbrains.isDeveloperMode import software.aws.toolkits.jetbrains.services.amazonq.apps.AmazonQAppInitContext import software.aws.toolkits.jetbrains.services.amazonq.apps.AppConnection import software.aws.toolkits.jetbrains.services.amazonq.commands.MessageTypeRegistry +import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.amazonq.lsp.artifacts.ArtifactManager import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AsyncChatUiListener import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage @@ -109,17 +110,21 @@ class AmazonQPanel(val project: Project, private val scope: CoroutineScope) : Di webviewContainer.add(wrapper) wrapper.setContent(loadingPanel) - ApplicationManager.getApplication().executeOnPooledThread { - val webUri = runBlocking { service().fetchArtifact(project).resolve("amazonq-ui.js").toUri() } - loadingPanel.stopLoading() - runInEdt { + scope.launch { + val webUri = service().fetchArtifact(project).resolve("amazonq-ui.js").toUri() + // wait for server to be running + AmazonQLspService.getInstance(project).instanceFlow.first() + + withContext(EDT) { browser.complete( - Browser(this, webUri, project).also { + Browser(this@AmazonQPanel, webUri, project).also { wrapper.setContent(it.component()) initConnections() connectUi(it) connectApps(it) + + loadingPanel.stopLoading() } ) } diff --git a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt index 91975a72284..0bfd683b00b 100644 --- a/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt +++ b/plugins/amazonq/chat/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/webview/Browser.kt @@ -11,7 +11,6 @@ import com.intellij.openapi.util.Disposer import com.intellij.ui.jcef.JBCefJSQuery import org.cef.CefApp import software.aws.toolkits.jetbrains.services.amazonq.CodeWhispererFeatureConfigService -import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.AwsServerCapabilitiesProvider import software.aws.toolkits.jetbrains.services.amazonq.lsp.flareChat.FlareUiMessage import software.aws.toolkits.jetbrains.services.amazonq.profile.QRegionProfile @@ -46,17 +45,16 @@ class Browser(parent: Disposable, private val webUri: URI, val project: Project) "mynah", AssetResourceHandler.AssetResourceHandlerFactory(), ) - AmazonQLspService.getInstance(project).addLspInitializeMessageListener { - loadWebView( - isCodeTransformAvailable, - isFeatureDevAvailable, - isDocAvailable, - isCodeScanAvailable, - isCodeTestAvailable, - highlightCommand, - activeProfile - ) - } + + loadWebView( + isCodeTransformAvailable, + isFeatureDevAvailable, + isDocAvailable, + isCodeScanAvailable, + isCodeTestAvailable, + highlightCommand, + activeProfile + ) } override fun dispose() { diff --git a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt index c1e0ab471e3..933027a3b36 100644 --- a/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt +++ b/plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt @@ -25,6 +25,10 @@ import com.intellij.util.net.JdkProxyProvider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.future.asCompletableFuture import kotlinx.coroutines.runBlocking import kotlinx.coroutines.sync.Mutex @@ -75,7 +79,6 @@ import java.net.Proxy import java.net.URI import java.nio.charset.StandardCharsets import java.nio.file.Files -import java.util.Collections import java.util.concurrent.Future import kotlin.time.Duration.Companion.seconds @@ -114,9 +117,8 @@ internal class LSPProcessListener : ProcessListener { @Service(Service.Level.PROJECT) class AmazonQLspService(private val project: Project, private val cs: CoroutineScope) : Disposable { - private val lspInitializedMessageReceivedListener = Collections.synchronizedList(mutableListOf()) - fun addLspInitializeMessageListener(listener: AmazonQInitializeMessageReceivedListener) = lspInitializedMessageReceivedListener.add(listener) - fun notifyInitializeMessageReceived() = lspInitializedMessageReceivedListener.forEach { it() } + private val _flowInstance = MutableSharedFlow(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val instanceFlow = _flowInstance.asSharedFlow().map { it.languageServer } private var instance: Deferred val capabilities @@ -140,7 +142,9 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS // wait for handshake to complete instance.initializeResult.join() - instance + instance.also { + _flowInstance.emit(it) + } } } catch (e: Exception) { LOG.warn(e) { "Failed to start LSP server" } @@ -324,7 +328,6 @@ private class AmazonQServerInstance(private val project: Project, private val cs if (message is ResponseMessage && message.result is AwsExtendedInitializeResult) { val result = message.result as AwsExtendedInitializeResult AwsServerCapabilitiesProvider.getInstance(project).setAwsServerCapabilities(result.getAwsServerCapabilities()) - AmazonQLspService.getInstance(project).notifyInitializeMessageReceived() } consumer?.consume(message) } From 57e9b46bfbf99583655af3e8b4dd5f9fc4c933f9 Mon Sep 17 00:00:00 2001 From: Richard Li Date: Fri, 9 May 2025 14:17:26 -0700 Subject: [PATCH 4/4] lint --- .../amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt index a5edf466292..1ceb7d148da 100644 --- a/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt +++ b/plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt @@ -12,10 +12,10 @@ import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent import com.intellij.openapi.vfs.newvfs.events.VFileEvent import com.intellij.openapi.vfs.writeText import com.intellij.testFramework.DisposableRule +import com.intellij.testFramework.LightVirtualFile import com.intellij.testFramework.fixtures.CodeInsightTestFixture import com.intellij.testFramework.fixtures.IdeaTestFixtureFactory import com.intellij.testFramework.replaceService -import com.intellij.testFramework.LightVirtualFile import io.mockk.every import io.mockk.mockk import io.mockk.mockkObject