diff --git a/clients/intellij/.idea/misc.xml b/clients/intellij/.idea/misc.xml index b0137f1..ac801d8 100644 --- a/clients/intellij/.idea/misc.xml +++ b/clients/intellij/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt index 940bae2..c00b010 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/AcceptCompletion.kt @@ -11,15 +11,15 @@ import com.tabbyml.intellijtabby.editor.InlineCompletionService class AcceptCompletion : AnAction() { override fun actionPerformed(e: AnActionEvent) { val inlineCompletionService = service() - val editor = e.getRequiredData(CommonDataKeys.EDITOR) - inlineCompletionService.accept(editor) + inlineCompletionService.accept() } override fun update(e: AnActionEvent) { val inlineCompletionService = service() - e.presentation.isEnabled = e.getData(CommonDataKeys.EDITOR) != null - && e.project != null - && inlineCompletionService.currentText != null + e.presentation.isEnabled = e.project != null + && e.getData(CommonDataKeys.EDITOR) != null + && inlineCompletionService.shownInlineCompletion != null + && e.getData(CommonDataKeys.EDITOR) == inlineCompletionService.shownInlineCompletion?.editor } override fun getActionUpdateThread(): ActionUpdateThread { diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt index 60d345f..c5a3e18 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/DismissCompletion.kt @@ -16,9 +16,10 @@ class DismissCompletion : AnAction() { override fun update(e: AnActionEvent) { val inlineCompletionService = service() - e.presentation.isEnabled = e.getData(CommonDataKeys.EDITOR) != null - && e.project != null - && inlineCompletionService.currentText != null + e.presentation.isEnabled = e.project != null + && e.getData(CommonDataKeys.EDITOR) != null + && inlineCompletionService.shownInlineCompletion != null + && e.getData(CommonDataKeys.EDITOR) == inlineCompletionService.shownInlineCompletion?.editor } override fun getActionUpdateThread(): ActionUpdateThread { diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt index 0062ff0..9b192f3 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/actions/TriggerCompletion.kt @@ -4,7 +4,6 @@ import com.intellij.openapi.actionSystem.ActionUpdateThread import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.CommonDataKeys -import com.intellij.openapi.application.invokeLater import com.intellij.openapi.components.service import com.tabbyml.intellijtabby.agent.AgentService import com.tabbyml.intellijtabby.editor.InlineCompletionService @@ -15,23 +14,17 @@ class TriggerCompletion : AnAction() { val agentService = service() val inlineCompletionService = service() val editor = e.getRequiredData(CommonDataKeys.EDITOR) - val file = e.getRequiredData(CommonDataKeys.PSI_FILE) val offset = editor.caretModel.primaryCaret.offset inlineCompletionService.dismiss() - agentService.getCompletion(editor, file, offset)?.thenAccept { - invokeLater { - inlineCompletionService.show(editor, offset, it) - } + agentService.getCompletion(editor, offset)?.thenAccept { + inlineCompletionService.show(editor, offset, it) } } override fun update(e: AnActionEvent) { - val inlineCompletionService = service() e.presentation.isEnabled = e.project != null && e.getData(CommonDataKeys.EDITOR) != null - && e.getData(CommonDataKeys.PSI_FILE) != null - && inlineCompletionService.currentText == null } override fun getActionUpdateThread(): ActionUpdateThread { diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt index e3de992..07c88e2 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/Agent.kt @@ -84,14 +84,14 @@ class Agent : ProcessAdapter() { ) } - fun getCompletions(request: CompletionRequest): CompletableFuture { + fun getCompletions(request: CompletionRequest): CompletableFuture { return request("getCompletions", listOf(request)) } private var requestId = 1 private var ongoingRequest = mutableMapOf Unit>() - private inline fun request(func: String, args: List = emptyList()): CompletableFuture { + private inline fun request(func: String, args: List = emptyList()): CompletableFuture { val id = requestId++ val data = listOf(id, mapOf("func" to func, "args" to args)) val json = gson.toJson(data) diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt index 138b3af..16eea84 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/agent/AgentService.kt @@ -1,8 +1,10 @@ package com.tabbyml.intellijtabby.agent +import com.intellij.openapi.application.ReadAction import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger import com.intellij.openapi.editor.Editor +import com.intellij.psi.PsiDocumentManager import com.intellij.psi.PsiFile import java.util.concurrent.CompletableFuture @@ -23,14 +25,20 @@ class AgentService { } } - fun getCompletion(editor: Editor, file: PsiFile, offset: Int): CompletableFuture? { - return agent.thenCompose { - it?.getCompletions(Agent.CompletionRequest( - file.virtualFile.path, - file.language.id, // FIXME: map language id - editor.document.text, - offset - )) + fun getCompletion(editor: Editor, offset: Int): CompletableFuture? { + return agent.thenCompose {agent -> + ReadAction.compute { + editor.project?.let { project -> + PsiDocumentManager.getInstance(project).getPsiFile(editor.document) + } + }?.let { file -> + agent?.getCompletions(Agent.CompletionRequest( + file.virtualFile.path, + file.language.id, // FIXME: map language id + editor.document.text, + offset + )) + } } } } \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/CompletionScheduler.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/CompletionScheduler.kt new file mode 100644 index 0000000..ff29a26 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/CompletionScheduler.kt @@ -0,0 +1,62 @@ +package com.tabbyml.intellijtabby.editor + +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.fileEditor.FileEditorManagerEvent +import com.intellij.openapi.fileEditor.FileEditorManagerListener +import com.intellij.openapi.project.Project +import com.tabbyml.intellijtabby.agent.AgentService +import java.util.* + +@Service +class CompletionScheduler { + private val logger = Logger.getInstance(CompletionScheduler::class.java) + + data class CompletionContext(val editor: Editor, val offset: Int, val timer: Timer) + + private var project: Project? = null + var scheduled: CompletionContext? = null + private set + + + fun schedule(editor: Editor, offset: Int) { + clear() + val agentService = service() + val inlineCompletionService = service() + inlineCompletionService.dismiss() + val timer = Timer() + timer.schedule(object : TimerTask() { + override fun run() { + logger.info("Scheduled completion task running") + agentService.getCompletion(editor, offset)?.thenAccept { + inlineCompletionService.show(editor, offset, it) + } + } + }, 150) + scheduled = CompletionContext(editor, offset, timer) + + if (project != editor.project) { + project = editor.project + editor.project?.messageBus?.connect()?.subscribe( + FileEditorManagerListener.FILE_EDITOR_MANAGER, + object: FileEditorManagerListener { + override fun selectionChanged(event: FileEditorManagerEvent) { + logger.info("FileEditorManagerListener selectionChanged.") + clear() + } + } + ) + } + } + + fun clear() { + scheduled?.let { + it.timer.cancel() + scheduled = null + } + val inlineCompletionService = service() + inlineCompletionService.dismiss() + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorListener.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorListener.kt new file mode 100644 index 0000000..c3e3605 --- /dev/null +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/EditorListener.kt @@ -0,0 +1,37 @@ +package com.tabbyml.intellijtabby.editor + +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.Logger +import com.intellij.openapi.editor.event.* +import com.intellij.openapi.fileEditor.FileEditorManager + +class EditorListener : EditorFactoryListener { + private val logger = Logger.getInstance(EditorListener::class.java) + + override fun editorCreated(event: EditorFactoryEvent) { + val editor = event.editor + val editorManager = editor.project?.let { FileEditorManager.getInstance(it) } ?: return + val completionScheduler = service() + + editor.caretModel.addCaretListener(object : CaretListener { + override fun caretPositionChanged(event: CaretEvent) { + if (editorManager.selectedTextEditor == editor) { + completionScheduler.scheduled?.let { + if (it.editor != editor || it.offset != editor.caretModel.primaryCaret.offset) { + completionScheduler.clear() + } + } + } + } + }) + + editor.document.addDocumentListener(object : DocumentListener { + override fun documentChanged(event: DocumentEvent) { + if (editorManager.selectedTextEditor == editor) { + val offset = event.offset + event.newFragment.length + completionScheduler.schedule(editor, offset) + } + } + }) + } +} \ No newline at end of file diff --git a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt index c0bbeae..fd06faf 100644 --- a/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt +++ b/clients/intellij/src/main/kotlin/com/tabbyml/intellijtabby/editor/InlineCompletionService.kt @@ -1,6 +1,6 @@ package com.tabbyml.intellijtabby.editor -import com.intellij.openapi.application.ReadAction +import com.intellij.openapi.application.invokeLater import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.components.Service import com.intellij.openapi.diagnostic.Logger @@ -14,50 +14,56 @@ import com.tabbyml.intellijtabby.agent.Agent import java.awt.Graphics import java.awt.Rectangle + @Service class InlineCompletionService { private val logger = Logger.getInstance(InlineCompletionService::class.java) - var currentText: String? = null + + data class InlineCompletion(val editor: Editor, val text: String, val offset: Int, val inlays: List>) + + var shownInlineCompletion: InlineCompletion? = null private set - var currentOffset: Int? = null - private set - private var currentInlays: MutableList> = mutableListOf() fun show(editor: Editor, offset: Int, completion: Agent.CompletionResponse) { + dismiss() if (completion.choices.isEmpty()) { return } - val text = completion.choices.first().text - logger.info("Showing inline completion at $offset: $text") - val lines = text.split("\n") - lines.forEachIndexed { index, line -> addInlayLine(editor, offset, line, index) } - currentText = text - currentOffset = offset + invokeLater { + val text = completion.choices.first().text + logger.info("Showing inline completion at $offset: $text") + val lines = text.split("\n") + val inlays = lines + .mapIndexed { index, line -> createInlayLine(editor, offset, line, index) } + .filterNotNull() + shownInlineCompletion = InlineCompletion(editor, text, offset, inlays) + } } - fun accept(editor: Editor) { - currentText?.let { - WriteCommandAction.runWriteCommandAction(editor.project) { - editor.document.insertString(currentOffset!!, it) - editor.caretModel.moveToOffset(currentOffset!! + it.length) + fun accept() { + shownInlineCompletion?.let { + logger.info("Accept inline completion at ${it.offset}: ${it.text}") + WriteCommandAction.runWriteCommandAction(it.editor.project) { + it.editor.document.insertString(it.offset, it.text) + it.editor.caretModel.moveToOffset(it.offset + it.text.length) } - currentText = null - currentOffset = null - currentInlays.forEach(Disposer::dispose) - currentInlays = mutableListOf() + invokeLater { + it.inlays.forEach(Disposer::dispose) + } + shownInlineCompletion = null } } fun dismiss() { - currentText?.let { - currentText = null - currentOffset = null - currentInlays.forEach(Disposer::dispose) - currentInlays = mutableListOf() + shownInlineCompletion?.let { + invokeLater { + it.inlays.forEach(Disposer::dispose) + } + shownInlineCompletion = null } } - private fun addInlayLine(editor: Editor, offset: Int, line: String, index: Int) { + private fun createInlayLine(editor: Editor, offset: Int, line: String, index: Int): Inlay<*>? { val renderer = object : EditorCustomElementRenderer { override fun calcWidthInPixels(inlay: Inlay<*>): Int { // FIXME: Calc width? @@ -69,13 +75,10 @@ class InlineCompletionService { graphics.drawString(line, targetRect.x, targetRect.y + inlay.editor.ascent) } } - val inlay = if (index == 0) { + return if (index == 0) { editor.inlayModel.addInlineElement(offset, true, renderer) } else { editor.inlayModel.addBlockElement(offset, true, false, -index, renderer) } - inlay?.let { - currentInlays.add(it) - } } } \ No newline at end of file diff --git a/clients/intellij/src/main/resources/META-INF/plugin.xml b/clients/intellij/src/main/resources/META-INF/plugin.xml index 1bbd904..6951d0f 100644 --- a/clients/intellij/src/main/resources/META-INF/plugin.xml +++ b/clients/intellij/src/main/resources/META-INF/plugin.xml @@ -25,26 +25,30 @@ + - - - - - - - - - + + + + + + + + + + + + \ No newline at end of file