Skip to content

Commit b9c1087

Browse files
committed
refactor: Improve completion handling and debouncing
- Implement atomic operations for thread safety - Add debouncer to reduce unnecessary completion triggers - Enhance error handling and job cancellation logic
1 parent a5f5138 commit b9c1087

File tree

2 files changed

+147
-104
lines changed

2 files changed

+147
-104
lines changed

src/main/kotlin/ai/devchat/plugin/completion/editor/CompletionProvider.kt

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,57 @@ import com.intellij.openapi.components.service
55
import com.intellij.openapi.diagnostic.Logger
66
import com.intellij.openapi.editor.Editor
77
import ai.devchat.plugin.completion.agent.AgentService
8-
import kotlinx.coroutines.Job
8+
import kotlinx.coroutines.*
99
import kotlinx.coroutines.flow.MutableStateFlow
1010
import kotlinx.coroutines.flow.asStateFlow
11-
import kotlinx.coroutines.launch
11+
import java.util.concurrent.atomic.AtomicInteger
12+
import java.util.concurrent.atomic.AtomicReference
1213

1314
@Service
1415
class CompletionProvider {
15-
private val logger = Logger.getInstance(CompletionProvider::class.java)
16+
private val logger = Logger.getInstance(CompletionProvider::class.java)
17+
private var completionSequence = AtomicInteger(0)
1618

17-
data class CompletionContext(val editor: Editor, val offset: Int, val job: Job)
19+
data class CompletionContext(val editor: Editor, val offset: Int, val job: Job)
1820

19-
private val ongoingCompletionFlow: MutableStateFlow<CompletionContext?> = MutableStateFlow(null)
20-
val ongoingCompletion = ongoingCompletionFlow.asStateFlow()
21+
private val ongoingCompletionFlow: MutableStateFlow<CompletionContext?> = MutableStateFlow(null)
22+
val ongoingCompletion = ongoingCompletionFlow.asStateFlow()
2123

22-
fun provideCompletion(editor: Editor, offset: Int, manually: Boolean = false) {
23-
val agentService = service<AgentService>()
24-
val inlineCompletionService = service<InlineCompletionService>()
25-
clear()
26-
val job = agentService.scope.launch {
27-
logger.info("Trigger completion at $offset")
28-
agentService.provideCompletion(editor, offset, manually).let {
29-
ongoingCompletionFlow.value = null
30-
if (it != null) {
31-
logger.info("Show completion at $offset: $it")
32-
inlineCompletionService.show(editor, offset, it)
24+
private val currentContext = AtomicReference<CompletionContext?>(null)
25+
26+
fun provideCompletion(editor: Editor, offset: Int, manually: Boolean = false) {
27+
val currentSequence = completionSequence.incrementAndGet()
28+
val agentService = service<AgentService>()
29+
val inlineCompletionService = service<InlineCompletionService>()
30+
31+
val oldContext = currentContext.getAndSet(null)
32+
oldContext?.job?.cancel()
33+
inlineCompletionService.dismiss()
34+
35+
val job = agentService.scope.launch {
36+
logger.info("Trigger completion at $offset")
37+
agentService.provideCompletion(editor, offset, manually).let {
38+
if (isActive) { // Check if the job is still active before updating
39+
if (it != null && completionSequence.get() == currentSequence) {
40+
logger.info("Show completion at $offset: $it")
41+
inlineCompletionService.show(editor, offset, it)
42+
}
43+
currentContext.set(null)
44+
ongoingCompletionFlow.value = null
45+
}
46+
}
3347
}
34-
}
48+
49+
val newContext = CompletionContext(editor, offset, job)
50+
currentContext.set(newContext)
51+
ongoingCompletionFlow.value = newContext
3552
}
36-
ongoingCompletionFlow.value = CompletionContext(editor, offset, job)
37-
}
38-
39-
fun clear() {
40-
val inlineCompletionService = service<InlineCompletionService>()
41-
inlineCompletionService.dismiss()
42-
ongoingCompletionFlow.value?.let {
43-
if (it.job.isActive) it.job.cancel()
44-
ongoingCompletionFlow.value = null
53+
54+
fun clear() {
55+
val inlineCompletionService = service<InlineCompletionService>()
56+
inlineCompletionService.dismiss()
57+
val context = currentContext.getAndSet(null)
58+
context?.job?.cancel()
59+
ongoingCompletionFlow.value = null
4560
}
46-
}
4761
}
Lines changed: 105 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,103 +1,132 @@
11
package ai.devchat.plugin.completion.editor
22

33
import ai.devchat.storage.CONFIG
4-
import ai.devchat.storage.CompletionTriggerMode
5-
import ai.devchat.storage.DevChatState
6-
import com.intellij.openapi.application.invokeLater
4+
import com.intellij.openapi.application.ApplicationManager
5+
import com.intellij.openapi.application.ModalityState
76
import com.intellij.openapi.components.service
87
import com.intellij.openapi.diagnostic.Logger
98
import com.intellij.openapi.editor.Editor
109
import com.intellij.openapi.editor.event.*
1110
import com.intellij.openapi.fileEditor.FileEditorManager
1211
import com.intellij.openapi.fileEditor.FileEditorManagerEvent
1312
import com.intellij.openapi.fileEditor.FileEditorManagerListener
13+
import kotlinx.coroutines.*
14+
import java.util.concurrent.atomic.AtomicLong
15+
16+
class Debouncer(private val debounceDelay: Long, private val scope: CoroutineScope) {
17+
private val lastTimestamp = AtomicLong(0)
18+
19+
fun debounce(action: suspend () -> Unit): Job = scope.launch {
20+
val timestamp = System.currentTimeMillis()
21+
lastTimestamp.set(timestamp)
22+
delay(debounceDelay)
23+
if (timestamp == lastTimestamp.get()) {
24+
action()
25+
}
26+
}
27+
}
1428

1529
class EditorListener : EditorFactoryListener {
16-
private val logger = Logger.getInstance(EditorListener::class.java)
17-
private val disposers = mutableMapOf<Editor, () -> Unit>()
30+
private val logger = Logger.getInstance(EditorListener::class.java)
31+
private val disposers = mutableMapOf<Editor, () -> Unit>()
32+
private val debouncer = Debouncer(300, CoroutineScope(Dispatchers.Default))
1833

19-
override fun editorCreated(event: EditorFactoryEvent) {
20-
val editor = event.editor
21-
val editorManager = editor.project?.let { FileEditorManager.getInstance(it) } ?: return
22-
val completionProvider = service<CompletionProvider>()
23-
val inlineCompletionService = service<InlineCompletionService>()
24-
logger.debug("EditorFactoryListener: editorCreated $event")
34+
override fun editorCreated(event: EditorFactoryEvent) {
35+
val editor = event.editor
36+
val editorManager = editor.project?.let { FileEditorManager.getInstance(it) } ?: return
37+
val completionProvider = service<CompletionProvider>()
38+
val inlineCompletionService = service<InlineCompletionService>()
39+
logger.debug("EditorFactoryListener: editorCreated $event")
2540

26-
editor.caretModel.addCaretListener(object : CaretListener {
27-
override fun caretPositionChanged(event: CaretEvent) {
28-
logger.debug("CaretListener: caretPositionChanged $event")
29-
if (editorManager.selectedTextEditor == editor) {
30-
inlineCompletionService.shownInlineCompletion?.let {
31-
if (it.ongoing) return
32-
}
33-
completionProvider.ongoingCompletion.value.let {
34-
if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) {
35-
// keep ongoing completion
36-
logger.debug("Keep ongoing completion.")
37-
} else {
38-
completionProvider.clear()
41+
editor.caretModel.addCaretListener(object : CaretListener {
42+
override fun caretPositionChanged(event: CaretEvent) {
43+
logger.debug("CaretListener: caretPositionChanged $event")
44+
if (editorManager.selectedTextEditor == editor) {
45+
inlineCompletionService.shownInlineCompletion?.let {
46+
if (it.ongoing) return
47+
}
48+
completionProvider.ongoingCompletion.value.let {
49+
if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) {
50+
logger.debug("Keep ongoing completion.")
51+
} else {
52+
completionProvider.clear()
53+
}
54+
}
55+
}
56+
}
57+
})
58+
59+
val documentListener = object : DocumentListener {
60+
override fun documentChanged(event: DocumentEvent) {
61+
logger.info("DocumentListener: documentChanged $event")
62+
63+
debouncer.debounce {
64+
ApplicationManager.getApplication().invokeLater({
65+
processDocumentChange(event, editor, editorManager, completionProvider, inlineCompletionService)
66+
}, ModalityState.defaultModalityState())
67+
}
3968
}
40-
}
4169
}
42-
}
43-
})
70+
editor.document.addDocumentListener(documentListener)
4471

45-
val documentListener = object : DocumentListener {
46-
override fun documentChanged(event: DocumentEvent) {
47-
logger.debug("DocumentListener: documentChanged $event")
48-
if (editorManager.selectedTextEditor == editor) {
49-
val enabled = CONFIG["complete_enable"] as? Boolean ?: false
50-
if (enabled) {
51-
inlineCompletionService.shownInlineCompletion?.let {
52-
if (it.ongoing) {
53-
logger.debug("DocumentListener: documentChanged $event, but ongoing inline completion.")
54-
return
55-
}
72+
val messagesConnection = editor.project?.messageBus?.connect()
73+
messagesConnection?.subscribe(
74+
FileEditorManagerListener.FILE_EDITOR_MANAGER,
75+
object : FileEditorManagerListener {
76+
override fun selectionChanged(event: FileEditorManagerEvent) {
77+
logger.debug("FileEditorManagerListener: selectionChanged.")
78+
completionProvider.clear()
79+
}
5680
}
81+
)
5782

83+
disposers[editor] = {
84+
editor.document.removeDocumentListener(documentListener)
85+
messagesConnection?.disconnect()
86+
}
87+
}
88+
89+
override fun editorReleased(event: EditorFactoryEvent) {
90+
logger.debug("EditorFactoryListener: editorReleased $event")
91+
disposers[event.editor]?.invoke()
92+
disposers.remove(event.editor)
93+
}
5894

95+
private fun processDocumentChange(
96+
event: DocumentEvent,
97+
editor: Editor,
98+
editorManager: FileEditorManager,
99+
completionProvider: CompletionProvider,
100+
inlineCompletionService: InlineCompletionService
101+
) {
102+
logger.info("trigger processDocumentChange")
103+
if (editorManager.selectedTextEditor == editor) {
104+
val enabled = CONFIG["complete_enable"] as? Boolean ?: false
105+
if (enabled) {
106+
inlineCompletionService.shownInlineCompletion?.let {
107+
if (it.ongoing) {
108+
logger.info("Ongoing inline completion, skipping.")
109+
return
110+
}
111+
}
59112

60-
completionProvider.ongoingCompletion.value.let {
61-
if (it != null && it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) {
62-
// keep ongoing completion
63-
logger.debug("Keep ongoing completion.")
64-
} else {
65-
logger.debug("DocumentListener: documentChanged $event, need to completion.")
66-
invokeLater {
67-
completionProvider.provideCompletion(editor, editor.caretModel.primaryCaret.offset)
113+
completionProvider.ongoingCompletion.value?.let {
114+
if (it.editor == editor && it.offset == editor.caretModel.primaryCaret.offset) {
115+
logger.info("Keeping ongoing completion.")
116+
} else {
117+
logger.info("Cancelling previous completion and providing new one.")
118+
completionProvider.clear()
119+
completionProvider.provideCompletion(editor, editor.caretModel.primaryCaret.offset)
120+
}
121+
} ?: run {
122+
logger.info("Providing new completion.")
123+
completionProvider.provideCompletion(editor, editor.caretModel.primaryCaret.offset)
68124
}
69-
}
125+
} else {
126+
logger.debug("Completion is disabled.")
70127
}
71-
} else {
72-
logger.debug("DocumentListener: documentChanged $event, but completion is disabled.")
73-
}
74128
} else {
75-
logger.debug("DocumentListener: documentChanged $event, but not selected editor.")
76-
}
77-
}
78-
}
79-
editor.document.addDocumentListener(documentListener)
80-
81-
val messagesConnection = editor.project?.messageBus?.connect()
82-
messagesConnection?.subscribe(
83-
FileEditorManagerListener.FILE_EDITOR_MANAGER,
84-
object : FileEditorManagerListener {
85-
override fun selectionChanged(event: FileEditorManagerEvent) {
86-
logger.debug("FileEditorManagerListener: selectionChanged.")
87-
completionProvider.clear()
129+
logger.debug("Not the selected editor.")
88130
}
89-
}
90-
)
91-
92-
disposers[editor] = {
93-
editor.document.removeDocumentListener(documentListener)
94-
messagesConnection?.disconnect()
95131
}
96-
}
97-
98-
override fun editorReleased(event: EditorFactoryEvent) {
99-
logger.debug("EditorFactoryListener: editorReleased $event")
100-
disposers[event.editor]?.invoke()
101-
disposers.remove(event.editor)
102-
}
103132
}

0 commit comments

Comments
 (0)