Skip to content

Commit f8e920e

Browse files
Merge main into feature/build-execute
2 parents 006a22c + 736ae74 commit f8e920e

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)