diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt new file mode 100644 index 00000000..b9089802 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt @@ -0,0 +1,23 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext +import com.intellij.openapi.actionSystem.ActionPromoter +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import ee.carlrobert.codegpt.codecompletions.AcceptNextLineInlayAction +import ee.carlrobert.codegpt.codecompletions.AcceptNextWordInlayAction + +class InlayActionPromoter : ActionPromoter { + override fun promote(actions: List, context: DataContext): List { + val editor = CommonDataKeys.EDITOR.getData(context) ?: return emptyList() + + if (InlineCompletionContext.getOrNull(editor) == null) { + return emptyList() + } + + actions.filterIsInstance().takeIf { it.isNotEmpty() }?.let { return it } + actions.filterIsInstance().takeIf { it.isNotEmpty() }?.let { return it } + return emptyList() + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AbstractInlayActionHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AbstractInlayActionHandler.kt new file mode 100644 index 00000000..b15ed65f --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AbstractInlayActionHandler.kt @@ -0,0 +1,68 @@ +package ee.carlrobert.codegpt.codecompletions + +import com.intellij.codeInsight.inline.completion.InlineCompletion +import com.intellij.codeInsight.inline.completion.InlineCompletionEvent +import com.intellij.codeInsight.inline.completion.InlineCompletionHandler +import com.intellij.codeInsight.inline.completion.InlineCompletionRequest +import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.runWriteAction +import com.intellij.openapi.editor.Caret +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler +import com.intellij.openapi.util.TextRange + +abstract class AbstractInlayActionHandler : EditorWriteActionHandler() { + + abstract fun InlineCompletionSession.getTextToInsert(): String + abstract fun InlineCompletionHandler.invoke(request: InlineCompletionRequest, range: TextRange) + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + InlineCompletionSession.getOrNull(editor)?.let { session -> + if (!session.isActive() || !session.context.isCurrentlyDisplaying()) return + + // TODO: Implement brace matching + InlineCompletion.getHandlerOrNull(editor)?.apply { + val startOffset = editor.caretModel.offset + val textToInsert = session.getTextToInsert() + + withIgnoringDocumentChanges { + val suggestionTextRange = + TextRange(startOffset - textToInsert.length, startOffset) + invoke(session.request, suggestionTextRange) + + runWriteAction { + editor.document.insertString(startOffset, textToInsert) + editor.caretModel.moveToOffset(startOffset + textToInsert.length) + } + } + } + } + } + + override fun isEnabledForCaret( + editor: Editor, + caret: Caret, + dataContext: DataContext? + ): Boolean = InlineCompletionSession.getOrNull(editor)?.let { + it.isActive() && it.context.isCurrentlyDisplaying() + } ?: false +} + +internal abstract class ApplyNextInlaySuggestionEvent( + private val originalRequest: InlineCompletionRequest, + private val textRange: TextRange +) : InlineCompletionEvent { + + override fun toRequest(): InlineCompletionRequest { + return InlineCompletionRequest( + this, + originalRequest.file, + originalRequest.editor, + originalRequest.document, + textRange.startOffset, + textRange.endOffset, + originalRequest.lookupElement + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AcceptNextLineInlayAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AcceptNextLineInlayAction.kt new file mode 100644 index 00000000..e43d2308 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AcceptNextLineInlayAction.kt @@ -0,0 +1,45 @@ +package ee.carlrobert.codegpt.codecompletions + +import ai.grazie.nlp.utils.takeWhitespaces +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.codeInsight.inline.completion.InlineCompletionHandler +import com.intellij.codeInsight.inline.completion.InlineCompletionRequest +import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.util.TextRange + +class AcceptNextLineInlayAction : EditorAction(Handler()), HintManagerImpl.ActionToIgnore { + + companion object { + const val ID = "codegpt.acceptNextInlayLine" + } + + private class Handler : AbstractInlayActionHandler() { + + override fun InlineCompletionSession.getTextToInsert(): String { + val completionText = context.textToInsert() + val lineBreakIndex = completionText.indexOfFirst { it == '\n' } + + return if (lineBreakIndex == -1) { + completionText + } else { + val nextLineStart = lineBreakIndex + 1 + val whitespacesLength = + completionText.substring(nextLineStart).takeWhitespaces().length + completionText.substring(0, nextLineStart + whitespacesLength) + } + } + + override fun InlineCompletionHandler.invoke( + request: InlineCompletionRequest, + range: TextRange + ) { + invokeEvent(ApplyNextLineInlaySuggestionEvent(request, range)) + } + } +} + +internal class ApplyNextLineInlaySuggestionEvent( + originalRequest: InlineCompletionRequest, + textRange: TextRange +) : ApplyNextInlaySuggestionEvent(originalRequest, textRange) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AcceptNextWordInlayAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AcceptNextWordInlayAction.kt new file mode 100644 index 00000000..ab9b6797 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/AcceptNextWordInlayAction.kt @@ -0,0 +1,34 @@ +package ee.carlrobert.codegpt.codecompletions + +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.codeInsight.inline.completion.InlineCompletionHandler +import com.intellij.codeInsight.inline.completion.InlineCompletionRequest +import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession +import com.intellij.openapi.editor.actionSystem.EditorAction +import com.intellij.openapi.util.TextRange + +class AcceptNextWordInlayAction : EditorAction(Handler()), HintManagerImpl.ActionToIgnore { + + companion object { + const val ID = "codegpt.acceptNextInlayWord" + } + + private class Handler : AbstractInlayActionHandler() { + + override fun InlineCompletionSession.getTextToInsert(): String { + return CompletionSplitter.split(context.textToInsert()) + } + + override fun InlineCompletionHandler.invoke( + request: InlineCompletionRequest, + range: TextRange + ) { + invokeEvent(ApplyNextWordInlaySuggestionEvent(request, range)) + } + } +} + +internal class ApplyNextWordInlaySuggestionEvent( + originalRequest: InlineCompletionRequest, + textRange: TextRange +) : ApplyNextInlaySuggestionEvent(originalRequest, textRange) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/ApplyNextWordInlayAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/ApplyNextWordInlayAction.kt deleted file mode 100644 index 6ae3df0b..00000000 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/ApplyNextWordInlayAction.kt +++ /dev/null @@ -1,65 +0,0 @@ -package ee.carlrobert.codegpt.codecompletions - -import com.intellij.codeInsight.inline.completion.InlineCompletion -import com.intellij.codeInsight.inline.completion.InlineCompletionEvent -import com.intellij.codeInsight.inline.completion.InlineCompletionHandler -import com.intellij.codeInsight.inline.completion.InlineCompletionRequest -import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext -import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession -import com.intellij.openapi.actionSystem.DataContext -import com.intellij.openapi.application.runWriteAction -import com.intellij.openapi.editor.Caret -import com.intellij.openapi.editor.Editor -import com.intellij.openapi.editor.actionSystem.EditorAction -import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler -import com.intellij.openapi.util.TextRange - -class ApplyNextWordInlayAction : EditorAction(Handler()) { - - private class Handler : EditorWriteActionHandler() { - override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { - InlineCompletionSession.getOrNull(editor)?.let { - if (!it.isActive() || !it.context.isCurrentlyDisplaying()) return - - InlineCompletion.getHandlerOrNull(editor)?.apply( - it.applyNextWordInlaySuggestion(it.context, editor) - ) - } - } - - private fun InlineCompletionSession.applyNextWordInlaySuggestion( - context: InlineCompletionContext, - editor: Editor - ): InlineCompletionHandler.() -> Unit = { - val startOffset = editor.caretModel.offset - val textToInsert = CompletionSplitter.split(context.textToInsert()) - withIgnoringDocumentChanges { - val suggestionTextRange = TextRange(startOffset - textToInsert.length, startOffset) - invokeEvent(ApplyNextWordInlaySuggestionEvent(request, suggestionTextRange)) - - runWriteAction { - editor.document.insertString(startOffset, textToInsert) - editor.caretModel.moveToOffset(startOffset + textToInsert.length) - } - } - } - } -} - -internal class ApplyNextWordInlaySuggestionEvent( - private val originalRequest: InlineCompletionRequest, - private val textRange: TextRange -) : InlineCompletionEvent { - - override fun toRequest(): InlineCompletionRequest { - return InlineCompletionRequest( - this, - originalRequest.file, - originalRequest.editor, - originalRequest.document, - textRange.startOffset, - textRange.endOffset, - originalRequest.lookupElement - ) - } -} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProviderPresentation.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProviderPresentation.kt index 64cee67b..002d7da4 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProviderPresentation.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProviderPresentation.kt @@ -1,10 +1,11 @@ package ee.carlrobert.codegpt.codecompletions import com.intellij.codeInsight.inline.completion.InlineCompletionProviderPresentation +import com.intellij.openapi.actionSystem.ActionManager import com.intellij.openapi.components.service +import com.intellij.openapi.keymap.KeymapUtil import com.intellij.openapi.project.Project import com.intellij.ui.components.JBLabel -import com.intellij.util.ui.JBFont import ee.carlrobert.codegpt.Icons import javax.swing.JComponent import javax.swing.SwingConstants @@ -15,10 +16,21 @@ class CodeCompletionProviderPresentation : InlineCompletionProviderPresentation val selectedModelCode = project?.service()?.getSelectedModelCode() ?: "" val text = if (selectedModelCode.isNotEmpty()) { - "CodeGPT: $selectedModelCode" + buildString { + append("Model: ($selectedModelCode) | ") + append("Accept Word: (${getShortcutText(AcceptNextWordInlayAction.ID)}) | ") + append("Accept Line: (${getShortcutText(AcceptNextLineInlayAction.ID)})") + } } else { "CodeGPT" } - return JBLabel(text, Icons.DefaultSmall, SwingConstants.LEADING).withFont(JBFont.small()) + + return JBLabel(text, Icons.DefaultSmall, SwingConstants.LEADING) + } + + private fun getShortcutText(actionId: String): String { + return KeymapUtil.getFirstKeyboardShortcutText( + ActionManager.getInstance().getAction(actionId) + ) } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionSuggestionUpdateAdapter.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionSuggestionUpdateAdapter.kt index bb4a9020..0e3cee0a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionSuggestionUpdateAdapter.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionSuggestionUpdateAdapter.kt @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.codecompletions +import ai.grazie.nlp.utils.takeWhitespaces import com.intellij.codeInsight.inline.completion.InlineCompletionEvent import com.intellij.codeInsight.inline.completion.elements.InlineCompletionGrayTextElement import com.intellij.codeInsight.inline.completion.suggestion.InlineCompletionSuggestionUpdateManager @@ -25,17 +26,50 @@ class CodeCompletionSuggestionUpdateAdapter : event: InlineCompletionEvent, variant: InlineCompletionVariant.Snapshot ): UpdateResult { - if (event !is ApplyNextWordInlaySuggestionEvent || variant.elements.isEmpty()) { - return Invalidated - } + if (variant.elements.isEmpty()) return Invalidated val completionText = variant.elements.joinToString("") { it.text } - val textToInsert = event.toRequest().run { - CompletionSplitter.split(completionText) + return when (event) { + is ApplyNextWordInlaySuggestionEvent -> handleNextWordEvent(event, variant, completionText) + is ApplyNextLineInlaySuggestionEvent -> handleNextLineEvent(event, variant, completionText) + else -> Invalidated } + } - updateRemainingCompletion(event.toRequest().editor, textToInsert) + private fun handleNextWordEvent( + event: ApplyNextWordInlaySuggestionEvent, + variant: InlineCompletionVariant.Snapshot, + completionText: String + ): UpdateResult { + val textToInsert = CompletionSplitter.split(completionText) + return createUpdatedVariant(event.toRequest().editor, variant, completionText, textToInsert) + } + private fun handleNextLineEvent( + event: ApplyNextLineInlaySuggestionEvent, + variant: InlineCompletionVariant.Snapshot, + completionText: String + ): UpdateResult { + val lineBreakIndex = completionText.indexOf('\n') + if (lineBreakIndex == -1) return Invalidated + + val nextLineStart = lineBreakIndex + 1 + val whitespacesLength = completionText + .substring(nextLineStart) + .takeWhitespaces() + .length + val textToInsert = completionText.substring(0, nextLineStart + whitespacesLength) + + return createUpdatedVariant(event.toRequest().editor, variant, completionText, textToInsert) + } + + private fun createUpdatedVariant( + editor: Editor, + variant: InlineCompletionVariant.Snapshot, + completionText: String, + textToInsert: String + ): UpdateResult { + updateRemainingCompletion(editor, textToInsert) return Changed( variant.copy( listOf(InlineCompletionGrayTextElement(completionText.removePrefix(textToInsert))) diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 227419b8..e8540da2 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -57,6 +57,7 @@ instance="ee.carlrobert.codegpt.telemetry.ui.preferences.TelemetryConfigurable" id="tools.preferences.codegpt.telemetry" displayName="Telemetry"/> + @@ -91,15 +92,16 @@ - - - - - - + use-shortcut-of="EditorNextWord" + class="ee.carlrobert.codegpt.codecompletions.AcceptNextWordInlayAction"/> + +