Skip to content

Commit 736ae74

Browse files
authored
fix(amazonq): only send textDocument/didOpen for explicitly focused editors (#5872)
* fix(amazonq): only send `textDocument/didOpen` for explicitly focused editors delay does not appear sufficent to satisfy LSP sensitivity to message floods on start (perhaps some weird stdio buffer overflow issue on windows) instead, match observed behavior of VSC/Eclipse and only send editors that have seen focus at least once * build * shadow
1 parent fed62c7 commit 736ae74

File tree

2 files changed

+93
-62
lines changed

2 files changed

+93
-62
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandler.kt

Lines changed: 50 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,14 @@ import com.intellij.openapi.fileEditor.FileEditorManagerEvent
1616
import com.intellij.openapi.fileEditor.FileEditorManagerListener
1717
import com.intellij.openapi.fileEditor.TextEditor
1818
import com.intellij.openapi.project.Project
19+
import com.intellij.openapi.util.Disposer
1920
import com.intellij.openapi.util.Key
2021
import com.intellij.openapi.vfs.VirtualFile
2122
import com.intellij.openapi.vfs.VirtualFileManager
2223
import com.intellij.openapi.vfs.newvfs.BulkFileListener
2324
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
2425
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
2526
import kotlinx.coroutines.CoroutineScope
26-
import kotlinx.coroutines.delay
2727
import kotlinx.coroutines.launch
2828
import org.eclipse.lsp4j.DidChangeTextDocumentParams
2929
import org.eclipse.lsp4j.DidCloseTextDocumentParams
@@ -46,6 +46,7 @@ class TextDocumentServiceHandler(
4646
BulkFileListener,
4747
DocumentListener,
4848
Disposable {
49+
4950
init {
5051
// didOpen & didClose events
5152
project.messageBus.connect(this).subscribe(
@@ -68,9 +69,8 @@ class TextDocumentServiceHandler(
6869
// open files on startup
6970
cs.launch {
7071
val fileEditorManager = FileEditorManager.getInstance(project)
71-
fileEditorManager.openFiles.forEach { file ->
72+
fileEditorManager.selectedFiles.forEach { file ->
7273
handleFileOpened(file)
73-
delay(100)
7474
}
7575
}
7676
}
@@ -84,23 +84,33 @@ class TextDocumentServiceHandler(
8484
}
8585
ApplicationManager.getApplication().runReadAction {
8686
FileDocumentManager.getInstance().getDocument(file)?.addDocumentListener(listener)
87-
file.putUserData(KEY_REAL_TIME_EDIT_LISTENER, listener)
8887
}
89-
}
88+
file.putUserData(KEY_REAL_TIME_EDIT_LISTENER, listener)
89+
90+
Disposer.register(this) {
91+
ApplicationManager.getApplication().runReadAction {
92+
val existingListener = file.getUserData(KEY_REAL_TIME_EDIT_LISTENER)
93+
if (existingListener != null) {
94+
FileDocumentManager.getInstance().getDocument(file)?.removeDocumentListener(existingListener)
95+
file.putUserData(KEY_REAL_TIME_EDIT_LISTENER, null)
96+
}
97+
}
98+
}
9099

91-
cs.launch {
92-
AmazonQLspService.executeAsyncIfRunning(project) { languageServer ->
93-
toUriString(file)?.let { uri ->
94-
languageServer.textDocumentService.didOpen(
95-
DidOpenTextDocumentParams().apply {
96-
textDocument = TextDocumentItem().apply {
97-
this.uri = uri
98-
text = file.inputStream.readAllBytes().decodeToString()
99-
languageId = file.fileType.name.lowercase()
100-
version = file.modificationStamp.toInt()
100+
cs.launch {
101+
AmazonQLspService.executeAsyncIfRunning(project) { languageServer ->
102+
toUriString(file)?.let { uri ->
103+
languageServer.textDocumentService.didOpen(
104+
DidOpenTextDocumentParams().apply {
105+
textDocument = TextDocumentItem().apply {
106+
this.uri = uri
107+
text = file.inputStream.readAllBytes().decodeToString()
108+
languageId = file.fileType.name.lowercase()
109+
version = file.modificationStamp.toInt()
110+
}
101111
}
102-
}
103-
)
112+
)
113+
}
104114
}
105115
}
106116
}
@@ -116,6 +126,7 @@ class TextDocumentServiceHandler(
116126
textDocument = TextDocumentIdentifier().apply {
117127
this.uri = uri
118128
}
129+
// TODO: should respect `textDocumentSync.save.includeText` server capability config
119130
text = document.text
120131
}
121132
)
@@ -125,10 +136,12 @@ class TextDocumentServiceHandler(
125136
}
126137

127138
override fun after(events: MutableList<out VFileEvent>) {
128-
cs.launch {
129-
AmazonQLspService.executeAsyncIfRunning(project) { languageServer ->
130-
events.filterIsInstance<VFileContentChangeEvent>().forEach { event ->
131-
val document = FileDocumentManager.getInstance().getCachedDocument(event.file) ?: return@forEach
139+
events.filterIsInstance<VFileContentChangeEvent>().forEach { event ->
140+
val document = FileDocumentManager.getInstance().getCachedDocument(event.file) ?: return@forEach
141+
142+
handleFileOpened(event.file)
143+
cs.launch {
144+
AmazonQLspService.executeAsyncIfRunning(project) { languageServer ->
132145
toUriString(event.file)?.let { uri ->
133146
languageServer.textDocumentService.didChange(
134147
DidChangeTextDocumentParams().apply {
@@ -164,17 +177,18 @@ class TextDocumentServiceHandler(
164177
if (listener != null) {
165178
FileDocumentManager.getInstance().getDocument(file)?.removeDocumentListener(listener)
166179
file.putUserData(KEY_REAL_TIME_EDIT_LISTENER, null)
167-
}
168-
cs.launch {
169-
AmazonQLspService.executeAsyncIfRunning(project) { languageServer ->
170-
toUriString(file)?.let { uri ->
171-
languageServer.textDocumentService.didClose(
172-
DidCloseTextDocumentParams().apply {
173-
textDocument = TextDocumentIdentifier().apply {
174-
this.uri = uri
180+
181+
cs.launch {
182+
AmazonQLspService.executeAsyncIfRunning(project) { languageServer ->
183+
toUriString(file)?.let { uri ->
184+
languageServer.textDocumentService.didClose(
185+
DidCloseTextDocumentParams().apply {
186+
textDocument = TextDocumentIdentifier().apply {
187+
this.uri = uri
188+
}
175189
}
176-
}
177-
)
190+
)
191+
}
178192
}
179193
}
180194
}
@@ -185,10 +199,12 @@ class TextDocumentServiceHandler(
185199
}
186200

187201
private fun handleActiveEditorChange(fileEditor: FileEditor?) {
202+
val editor = (fileEditor as? TextEditor)?.editor ?: return
203+
editor.virtualFile?.let { handleFileOpened(it) }
204+
188205
// Extract text editor if it's a TextEditor, otherwise null
189-
val editor = (fileEditor as? TextEditor)?.editor
190-
val textDocumentIdentifier = editor?.let { TextDocumentIdentifier(toUriString(it.virtualFile)) }
191-
val cursorState = editor?.let { getCursorState(it) }
206+
val textDocumentIdentifier = TextDocumentIdentifier(toUriString(editor.virtualFile))
207+
val cursorState = getCursorState(editor)
192208

193209
val params = mapOf(
194210
"textDocument" to textDocumentIdentifier,

plugins/amazonq/shared/jetbrains-community/tst/software/aws/toolkits/jetbrains/services/amazonq/lsp/textdocument/TextDocumentServiceHandlerTest.kt

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.textdocument
66
import com.intellij.openapi.application.writeAction
77
import com.intellij.openapi.editor.Document
88
import com.intellij.openapi.fileEditor.FileDocumentManager
9+
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx
910
import com.intellij.openapi.fileTypes.FileType
11+
import com.intellij.openapi.util.Disposer
1012
import com.intellij.openapi.vfs.VirtualFile
1113
import com.intellij.openapi.vfs.newvfs.events.VFileContentChangeEvent
1214
import com.intellij.openapi.vfs.newvfs.events.VFileEvent
@@ -24,7 +26,6 @@ import io.mockk.mockkStatic
2426
import io.mockk.slot
2527
import io.mockk.spyk
2628
import io.mockk.verify
27-
import kotlinx.coroutines.test.TestScope
2829
import kotlinx.coroutines.test.advanceUntilIdle
2930
import kotlinx.coroutines.test.runTest
3031
import kotlinx.coroutines.withContext
@@ -35,9 +36,11 @@ import org.eclipse.lsp4j.DidOpenTextDocumentParams
3536
import org.eclipse.lsp4j.DidSaveTextDocumentParams
3637
import org.eclipse.lsp4j.jsonrpc.messages.ResponseMessage
3738
import org.eclipse.lsp4j.services.TextDocumentService
39+
import org.junit.After
3840
import org.junit.Before
3941
import org.junit.Rule
4042
import org.junit.Test
43+
import org.junit.rules.TestName
4144
import software.aws.toolkits.jetbrains.core.coroutines.EDT
4245
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLanguageServer
4346
import software.aws.toolkits.jetbrains.services.amazonq.lsp.AmazonQLspService
@@ -54,9 +57,6 @@ class TextDocumentServiceHandlerTest {
5457
private lateinit var mockTextDocumentService: TextDocumentService
5558
private lateinit var sut: TextDocumentServiceHandler
5659

57-
// not ideal
58-
private lateinit var testScope: TestScope
59-
6060
@get:Rule
6161
val projectRule = object : CodeInsightTestFixtureRule() {
6262
override fun createTestFixture(): CodeInsightTestFixture {
@@ -74,6 +74,9 @@ class TextDocumentServiceHandlerTest {
7474
@get:Rule
7575
val disposableRule = DisposableRule()
7676

77+
@get:Rule
78+
val testName = TestName()
79+
7780
@Before
7881
fun setup() {
7982
mockTextDocumentService = mockk<TextDocumentService>()
@@ -99,13 +102,20 @@ class TextDocumentServiceHandlerTest {
99102
every { mockTextDocumentService.didSave(any()) } returns Unit
100103
every { mockTextDocumentService.didOpen(any()) } returns Unit
101104
every { mockTextDocumentService.didClose(any()) } returns Unit
105+
}
102106

103-
testScope = TestScope()
104-
sut = TextDocumentServiceHandler(projectRule.project, testScope)
107+
@After
108+
fun tearDown() {
109+
try {
110+
Disposer.dispose(sut)
111+
} catch (_: Exception) {
112+
}
105113
}
106114

107115
@Test
108-
fun `didSave runs on beforeDocumentSaving`() {
116+
fun `didSave runs on beforeDocumentSaving`() = runTest {
117+
sut = TextDocumentServiceHandler(projectRule.project, this)
118+
109119
// Create test document and file
110120
val uri = URI.create("file:///test/path/file.txt")
111121
val document = mockk<Document> {
@@ -127,7 +137,7 @@ class TextDocumentServiceHandlerTest {
127137
sut.beforeDocumentSaving(document)
128138

129139
// Verify the correct LSP method was called with matching parameters
130-
testScope.advanceUntilIdle()
140+
advanceUntilIdle()
131141
val paramsSlot = slot<DidSaveTextDocumentParams>()
132142
verify { mockTextDocumentService.didSave(capture(paramsSlot)) }
133143

@@ -142,12 +152,12 @@ class TextDocumentServiceHandlerTest {
142152
fun `didOpen runs on service init`() = runTest {
143153
val content = "test content"
144154
val file = withContext(EDT) {
145-
projectRule.fixture.createFile("name", content).also { projectRule.fixture.openFileInEditor(it) }
155+
projectRule.fixture.createFile(testName.methodName, content).also { projectRule.fixture.openFileInEditor(it) }
146156
}
147-
157+
advanceUntilIdle()
148158
sut = TextDocumentServiceHandler(projectRule.project, this)
149-
150159
advanceUntilIdle()
160+
151161
val paramsSlot = mutableListOf<DidOpenTextDocumentParams>()
152162
verify { mockTextDocumentService.didOpen(capture(paramsSlot)) }
153163

@@ -160,15 +170,14 @@ class TextDocumentServiceHandlerTest {
160170

161171
@Test
162172
fun `didOpen runs on fileOpened`() = runTest {
173+
sut = TextDocumentServiceHandler(projectRule.project, this)
174+
advanceUntilIdle()
163175
val content = "test content"
164176
val file = withContext(EDT) {
165-
projectRule.fixture.createFile("name", content).also { projectRule.fixture.openFileInEditor(it) }
177+
projectRule.fixture.createFile(testName.methodName, content).also { projectRule.fixture.openFileInEditor(it) }
166178
}
167-
168-
sut.fileOpened(mockk(), file)
169-
170179
advanceUntilIdle()
171-
testScope.advanceUntilIdle()
180+
172181
val paramsSlot = mutableListOf<DidOpenTextDocumentParams>()
173182
verify { mockTextDocumentService.didOpen(capture(paramsSlot)) }
174183

@@ -181,23 +190,26 @@ class TextDocumentServiceHandlerTest {
181190

182191
@Test
183192
fun `didClose runs on fileClosed`() = runTest {
184-
val uri = URI.create("file:///test/path/file.txt")
185-
val file = createMockVirtualFile(uri)
186-
187-
sut.fileClosed(mockk(), file)
193+
sut = TextDocumentServiceHandler(projectRule.project, this)
194+
val file = withContext(EDT) {
195+
projectRule.fixture.createFile(testName.methodName, "").also {
196+
projectRule.fixture.openFileInEditor(it)
197+
FileEditorManagerEx.getInstanceEx(projectRule.project).closeAllFiles()
198+
}
199+
}
188200

189201
advanceUntilIdle()
190-
testScope.advanceUntilIdle()
191202
val paramsSlot = slot<DidCloseTextDocumentParams>()
192203
verify { mockTextDocumentService.didClose(capture(paramsSlot)) }
193204

194-
assertThat(paramsSlot.captured.textDocument.uri).isEqualTo(normalizeFileUri(uri.toString()))
205+
assertThat(paramsSlot.captured.textDocument.uri).isEqualTo(file.toNioPath().toUri().toString())
195206
}
196207

197208
@Test
198209
fun `didChange runs on content change events`() = runTest {
210+
sut = TextDocumentServiceHandler(projectRule.project, this)
199211
val file = withContext(EDT) {
200-
projectRule.fixture.createFile("name", "").also {
212+
projectRule.fixture.createFile(testName.methodName, "").also {
201213
projectRule.fixture.openFileInEditor(it)
202214

203215
writeAction {
@@ -208,7 +220,6 @@ class TextDocumentServiceHandlerTest {
208220

209221
// Verify the correct LSP method was called with matching parameters
210222
advanceUntilIdle()
211-
testScope.advanceUntilIdle()
212223
val paramsSlot = mutableListOf<DidChangeTextDocumentParams>()
213224
verify { mockTextDocumentService.didChange(capture(paramsSlot)) }
214225

@@ -220,6 +231,7 @@ class TextDocumentServiceHandlerTest {
220231

221232
@Test
222233
fun `didSave does not run when URI is empty`() = runTest {
234+
sut = TextDocumentServiceHandler(projectRule.project, this)
223235
val document = mockk<Document>()
224236
val file = createMockVirtualFile(URI.create(""))
225237

@@ -235,14 +247,15 @@ class TextDocumentServiceHandlerTest {
235247

236248
sut.beforeDocumentSaving(document)
237249

238-
testScope.advanceUntilIdle()
250+
advanceUntilIdle()
239251
verify(exactly = 0) { mockTextDocumentService.didSave(any()) }
240252
}
241253
}
242254
}
243255

244256
@Test
245257
fun `didSave does not run when file is null`() = runTest {
258+
sut = TextDocumentServiceHandler(projectRule.project, this)
246259
val document = mockk<Document>()
247260

248261
val fileDocumentManager = mockk<FileDocumentManager> {
@@ -254,23 +267,25 @@ class TextDocumentServiceHandlerTest {
254267

255268
sut.beforeDocumentSaving(document)
256269

257-
testScope.advanceUntilIdle()
270+
advanceUntilIdle()
258271
verify(exactly = 0) { mockTextDocumentService.didSave(any()) }
259272
}
260273
}
261274

262275
@Test
263276
fun `didChange ignores non-content change events`() = runTest {
277+
sut = TextDocumentServiceHandler(projectRule.project, this)
264278
val nonContentEvent = mockk<VFileEvent>() // Some other type of VFileEvent
265279

266280
sut.after(mutableListOf(nonContentEvent))
267281

268-
testScope.advanceUntilIdle()
282+
advanceUntilIdle()
269283
verify(exactly = 0) { mockTextDocumentService.didChange(any()) }
270284
}
271285

272286
@Test
273287
fun `didChange skips files without cached documents`() = runTest {
288+
sut = TextDocumentServiceHandler(projectRule.project, this)
274289
val uri = URI.create("file:///test/path/file.txt")
275290
val path = mockk<Path> {
276291
every { toUri() } returns uri
@@ -291,7 +306,7 @@ class TextDocumentServiceHandlerTest {
291306

292307
sut.after(mutableListOf(changeEvent))
293308

294-
testScope.advanceUntilIdle()
309+
advanceUntilIdle()
295310
verify(exactly = 0) { mockTextDocumentService.didChange(any()) }
296311
}
297312
}

0 commit comments

Comments
 (0)