diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3e830f07..5a40fdc0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,7 +12,7 @@ jsoup = "1.17.2" jtokkit = "1.1.0" junit = "5.11.0" kotlin = "2.0.0" -llm-client = "0.8.30" +llm-client = "0.8.32" okio = "3.9.0" tree-sitter = "0.24.4" diff --git a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java index ec32dbcd..3420ebad 100644 --- a/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java +++ b/src/main/java/ee/carlrobert/codegpt/CodeGPTKeys.java @@ -1,10 +1,12 @@ package ee.carlrobert.codegpt; import com.intellij.openapi.util.Key; +import ee.carlrobert.codegpt.predictions.CodeSuggestionDiffViewer; import ee.carlrobert.codegpt.settings.prompts.PersonaDetails; import ee.carlrobert.codegpt.ui.DocumentationDetails; import ee.carlrobert.llm.client.codegpt.CodeGPTUserDetails; import java.util.List; +import okhttp3.Call; public class CodeGPTKeys { @@ -26,4 +28,8 @@ public class CodeGPTKeys { Key.create("codegpt.isFetchingCompletion"); public static final Key IS_PROMPT_TEXT_FIELD_DOCUMENT = Key.create("codegpt.isPromptTextFieldDocument"); + public static final Key EDITOR_PREDICTION_DIFF_VIEWER = + Key.create("codegpt.editorPredictionDiffViewer"); + public static final Key PENDING_PREDICTION_CALL = + Key.create("codegpt.editorPendingPredictionCall"); } diff --git a/src/main/java/ee/carlrobert/codegpt/statusbar/CodeGPTStatusBarWidget.java b/src/main/java/ee/carlrobert/codegpt/statusbar/CodeGPTStatusBarWidget.java index a3d9db2d..d0427b80 100644 --- a/src/main/java/ee/carlrobert/codegpt/statusbar/CodeGPTStatusBarWidget.java +++ b/src/main/java/ee/carlrobert/codegpt/statusbar/CodeGPTStatusBarWidget.java @@ -1,6 +1,7 @@ package ee.carlrobert.codegpt.statusbar; import static ee.carlrobert.codegpt.CodeGPTKeys.IS_FETCHING_COMPLETION; +import static ee.carlrobert.codegpt.CodeGPTKeys.PENDING_PREDICTION_CALL; import com.intellij.openapi.actionSystem.ActionGroup; import com.intellij.openapi.actionSystem.ActionManager; @@ -44,7 +45,9 @@ public class CodeGPTStatusBarWidget extends EditorBasedStatusBarPopup { protected @NotNull WidgetState getWidgetState(@Nullable VirtualFile file) { var state = new WidgetState(CodeGPTBundle.get("statusBar.widget.tooltip"), "", true); var fetchingCompletion = IS_FETCHING_COMPLETION.get(getEditor()); - var loading = fetchingCompletion != null && fetchingCompletion; + var pendingPredicationCall = PENDING_PREDICTION_CALL.get(getEditor()); + var loading = + (fetchingCompletion != null && fetchingCompletion) || pendingPredicationCall != null; state.setIcon(loading ? Icons.StatusBarCompletionInProgress : Icons.DefaultSmall); return state; diff --git a/src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java b/src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java index 791b7c57..7f240a4d 100644 --- a/src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/ui/UIUtil.java @@ -139,8 +139,14 @@ public class UIUtil { } public static JLabel createComment(String messageKey) { + return createComment(messageKey, ComponentPanelBuilder.MAX_COMMENT_WIDTH); + } + + public static JLabel createComment(String messageKey, int maxLineLength) { var comment = ComponentPanelBuilder.createCommentComponent( - CodeGPTBundle.get(messageKey), true); + CodeGPTBundle.get(messageKey), + true, + maxLineLength); comment.setBorder(JBUI.Borders.empty(0, 4)); return comment; } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt new file mode 100644 index 00000000..399684b8 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/CodeGPTLookupListener.kt @@ -0,0 +1,54 @@ +package ee.carlrobert.codegpt + +import com.intellij.codeInsight.lookup.Lookup +import com.intellij.codeInsight.lookup.LookupEvent +import com.intellij.codeInsight.lookup.LookupListener +import com.intellij.codeInsight.lookup.LookupManagerListener +import com.intellij.codeInsight.lookup.impl.LookupImpl +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.components.service +import ee.carlrobert.codegpt.predictions.PredictionService +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings + +class CodeGPTLookupListener : LookupManagerListener { + override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) { + if (newLookup is LookupImpl) { + newLookup.addLookupListener(object : LookupListener { + + var beforeApply: String = "" + var cursorOffset: Int = 0 + + override fun beforeItemSelected(event: LookupEvent): Boolean { + beforeApply = newLookup.editor.document.text + cursorOffset = runReadAction { + newLookup.editor.caretModel.offset + } + + return true + } + + override fun itemSelected(event: LookupEvent) { + val editor = newLookup.editor + + if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT + || !service().state.codeAssistantEnabled + || service().countTokens(editor.document.text) > 4098 + ) { + return + } + + ApplicationManager.getApplication().executeOnPooledThread { + service().displayLookupPrediction( + editor, + event, + beforeApply + ) + } + } + }) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeAssistantFeatureToggleActions.kt.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeAssistantFeatureToggleActions.kt.kt new file mode 100644 index 00000000..2a254d37 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/CodeAssistantFeatureToggleActions.kt.kt @@ -0,0 +1,35 @@ +package ee.carlrobert.codegpt.actions + +import com.intellij.openapi.actionSystem.ActionUpdateThread +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.components.service +import com.intellij.openapi.project.DumbAwareAction +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType.CODEGPT +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings + +abstract class CodeAssistantFeatureToggleAction( + private val enableFeatureAction: Boolean +) : DumbAwareAction() { + + override fun actionPerformed(e: AnActionEvent) { + val settings = service().state + settings.codeAssistantEnabled = enableFeatureAction + } + + override fun update(e: AnActionEvent) { + val codeAssistantEnabled = service().state.codeAssistantEnabled + + e.presentation.isVisible = GeneralSettings.getSelectedService() == CODEGPT + && codeAssistantEnabled != enableFeatureAction + e.presentation.isEnabled = GeneralSettings.getSelectedService() == CODEGPT + } + + override fun getActionUpdateThread(): ActionUpdateThread { + return ActionUpdateThread.BGT + } +} + +class EnableCodeAssistantAction : CodeAssistantFeatureToggleAction(true) + +class DisableCodeAssistantAction : CodeAssistantFeatureToggleAction(false) \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt index b9089802..ce000818 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/InlayActionPromoter.kt @@ -7,11 +7,14 @@ import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.DataContext import ee.carlrobert.codegpt.codecompletions.AcceptNextLineInlayAction import ee.carlrobert.codegpt.codecompletions.AcceptNextWordInlayAction +import ee.carlrobert.codegpt.predictions.TriggerCustomPredictionAction class InlayActionPromoter : ActionPromoter { override fun promote(actions: List, context: DataContext): List { val editor = CommonDataKeys.EDITOR.getData(context) ?: return emptyList() + actions.filterIsInstance().takeIf { it.isNotEmpty() }?.let { return it } + if (InlineCompletionContext.getOrNull(editor) == null) { return emptyList() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionInsertAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionInsertAction.kt index 07a36fed..1503b2d3 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionInsertAction.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionInsertAction.kt @@ -8,6 +8,8 @@ import com.intellij.codeInsight.inline.completion.session.InlineCompletionContex import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession import com.intellij.codeInsight.lookup.LookupManager import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service import com.intellij.openapi.editor.Caret import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.actionSystem.EditorAction @@ -15,6 +17,11 @@ import com.intellij.openapi.editor.actionSystem.EditorWriteActionHandler import com.intellij.psi.PsiDocumentManager import com.intellij.util.concurrency.ThreadingAssertions import ee.carlrobert.codegpt.CodeGPTKeys.REMAINING_EDITOR_COMPLETION +import ee.carlrobert.codegpt.EncodingManager +import ee.carlrobert.codegpt.predictions.PredictionService +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings class CodeCompletionInsertAction : EditorAction(InsertInlineCompletionHandler()), HintManagerImpl.ActionToIgnore { @@ -34,11 +41,27 @@ class CodeCompletionInsertAction : val textToInsert = context.textToInsert() val remainingCompletion = REMAINING_EDITOR_COMPLETION.get(editor) ?: "" if (remainingCompletion.isNotEmpty()) { - REMAINING_EDITOR_COMPLETION.set(editor, remainingCompletion.removePrefix(textToInsert)) + REMAINING_EDITOR_COMPLETION.set( + editor, + remainingCompletion.removePrefix(textToInsert) + ) } + val beforeApply = editor.document.text InlineCompletion.getHandlerOrNull(editor)?.insert() - return + + if (GeneralSettings.getSelectedService() == ServiceType.CODEGPT + && service().state.codeAssistantEnabled + && service().countTokens(editor.document.text) <= 4098) { + ApplicationManager.getApplication().executeOnPooledThread { + service().displayAutocompletePrediction( + editor, + textToInsert, + beforeApply + ) + } + return + } } for (element in elements) { @@ -92,7 +115,10 @@ class CodeCompletionInsertAction : processRemainingCompletion(remainingCompletionLine, editor, endOffset) } - private fun processPartialCompletionElement(element: CodeCompletionTextElement, editor: Editor) { + private fun processPartialCompletionElement( + element: CodeCompletionTextElement, + editor: Editor + ) { val lineNumber = editor.document.getLineNumber(editor.caretModel.offset) val lineEndOffset = editor.document.getLineEndOffset(lineNumber) editor.caretModel.moveToOffset(lineEndOffset) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProgressNotifier.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProgressNotifier.kt index c8cbc402..3908651a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProgressNotifier.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionProgressNotifier.kt @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.codecompletions +import com.intellij.openapi.project.Project import com.intellij.util.messages.Topic interface CodeCompletionProgressNotifier { @@ -10,5 +11,20 @@ interface CodeCompletionProgressNotifier { @JvmStatic val CODE_COMPLETION_PROGRESS_TOPIC = Topic.create("codeCompletionProgressTopic", CodeCompletionProgressNotifier::class.java) + + fun startLoading(project: Project) { + handleLoading(project, true) + } + + fun stopLoading(project: Project) { + handleLoading(project, false) + } + + private fun handleLoading(project: Project, loading: Boolean) { + if (project.isDisposed) return + project.messageBus + .syncPublisher(CODE_COMPLETION_PROGRESS_TOPIC) + ?.loading(loading) + } } } \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt index 80afd879..e3bd9d44 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/DebouncedCodeCompletionProvider.kt @@ -117,9 +117,10 @@ class DebouncedCodeCompletionProvider : DebouncedInlineCompletionProvider() { } IS_FETCHING_COMPLETION.set(request.editor, true) - request.editor.project?.messageBus - ?.syncPublisher(CodeCompletionProgressNotifier.CODE_COMPLETION_PROGRESS_TOPIC) - ?.loading(true) + + request.editor.project?.let { + CodeCompletionProgressNotifier.startLoading(it) + } return InlineCompletionSingleSuggestion.build(elements = channelFlow { val infillRequest = InfillRequestUtil.buildInfillRequest(request, completionType) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt index aea72abc..fe1d0856 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/codecompletions/InfillRequestUtil.kt @@ -3,21 +3,14 @@ package ee.carlrobert.codegpt.codecompletions import com.intellij.codeInsight.inline.completion.InlineCompletionRequest import com.intellij.openapi.application.readAction import com.intellij.openapi.components.service -import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder -import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter -import com.intellij.openapi.vcs.VcsException -import com.intellij.openapi.vcs.changes.ChangeListManager import com.intellij.refactoring.suggested.startOffset import ee.carlrobert.codegpt.EncodingManager import ee.carlrobert.codegpt.codecompletions.psi.CompletionContextService import ee.carlrobert.codegpt.codecompletions.psi.readText import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings import ee.carlrobert.codegpt.util.GitUtil -import java.io.StringWriter object InfillRequestUtil { - private val logger = thisLogger() suspend fun buildInfillRequest( request: InlineCompletionRequest, @@ -34,24 +27,9 @@ object InfillRequestUtil { val project = request.editor.project ?: return infillRequestBuilder.build() if (service().state.codeCompletionSettings.gitDiffEnabled) { - GitUtil.getProjectRepository(project)?.let { repository -> - try { - val repoRootPath = repository.root.toNioPath() - val changes = ChangeListManager.getInstance(project).allChanges - .sortedBy { it.virtualFile?.timeStamp } - val patches = IdeaTextPatchBuilder.buildPatch( - project, changes, repoRootPath, false, true - ) - val diffWriter = StringWriter() - UnifiedDiffWriter.write( - null, repoRootPath, patches, diffWriter, "\n\n", null, null - ) - val additionalContext = - diffWriter.toString().cleanDiff().truncateText(1024, false) - infillRequestBuilder.additionalContext(additionalContext) - } catch (e: VcsException) { - logger.error("Failed to get git context", e) - } + val additionalContext = GitUtil.getCurrentChanges(project) + if (!additionalContext.isNullOrEmpty()) { + infillRequestBuilder.additionalContext(additionalContext) } } @@ -62,18 +40,6 @@ object InfillRequestUtil { return infillRequestBuilder.build() } - private fun String.cleanDiff(showContext: Boolean = false): String = - lineSequence() - .filterNot { line -> - line.startsWith("index ") || - line.startsWith("diff --git") || - line.startsWith("---") || - line.startsWith("+++") || - line.startsWith("===") || - (!showContext && line.startsWith(" ")) - } - .joinToString("\n") - private fun getInfillContext( request: InlineCompletionRequest, caretOffset: Int diff --git a/src/main/kotlin/ee/carlrobert/codegpt/predictions/AcceptNextPredictionRevisionAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/predictions/AcceptNextPredictionRevisionAction.kt new file mode 100644 index 00000000..2998f7ff --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/predictions/AcceptNextPredictionRevisionAction.kt @@ -0,0 +1,32 @@ +package ee.carlrobert.codegpt.predictions + +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.openapi.actionSystem.DataContext +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 ee.carlrobert.codegpt.CodeGPTKeys + +class AcceptNextPredictionRevisionAction : EditorAction(Handler()), HintManagerImpl.ActionToIgnore { + + companion object { + const val ID = "codegpt.acceptNextPrediction" + } + + private class Handler : EditorWriteActionHandler() { + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + val diffViewer = editor.getUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER) + if (diffViewer != null && diffViewer.isVisible()) { + diffViewer.applyChanges() + return + } + } + + override fun isEnabledForCaret(editor: Editor, caret: Caret, dataContext: DataContext): Boolean { + val diffViewer = editor.getUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER) + return diffViewer != null && diffViewer.isVisible() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/predictions/CodeSuggestionDiffViewer.kt b/src/main/kotlin/ee/carlrobert/codegpt/predictions/CodeSuggestionDiffViewer.kt new file mode 100644 index 00000000..51586765 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/predictions/CodeSuggestionDiffViewer.kt @@ -0,0 +1,340 @@ +package ee.carlrobert.codegpt.predictions + +import com.intellij.codeInsight.inline.completion.session.InlineCompletionContext +import com.intellij.codeInsight.inline.completion.session.InlineCompletionSession +import com.intellij.diff.DiffContentFactory +import com.intellij.diff.DiffContext +import com.intellij.diff.requests.DiffRequest +import com.intellij.diff.requests.SimpleDiffRequest +import com.intellij.diff.tools.combined.COMBINED_DIFF_MAIN_UI +import com.intellij.diff.tools.fragmented.UnifiedDiffChange +import com.intellij.diff.tools.fragmented.UnifiedDiffViewer +import com.intellij.diff.util.DiffUserDataKeysEx.ScrollToPolicy +import com.intellij.diff.util.DiffUtil +import com.intellij.diff.util.Side +import com.intellij.ide.plugins.newui.TagComponent +import com.intellij.openapi.Disposable +import com.intellij.openapi.actionSystem.ActionManager +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.VisibleAreaEvent +import com.intellij.openapi.editor.event.VisibleAreaListener +import com.intellij.openapi.keymap.KeymapUtil +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.util.Key +import com.intellij.openapi.util.TextRange +import com.intellij.openapi.util.UserDataHolder +import com.intellij.openapi.util.UserDataHolderBase +import com.intellij.testFramework.LightVirtualFile +import com.intellij.ui.components.JBScrollPane +import com.intellij.util.concurrency.annotations.RequiresEdt +import com.intellij.util.containers.ContainerUtil +import com.intellij.util.ui.JBUI +import com.intellij.util.ui.components.BorderLayoutPanel +import ee.carlrobert.codegpt.CodeGPTKeys +import java.awt.Dimension +import java.awt.FlowLayout +import java.awt.Point +import java.awt.event.KeyAdapter +import java.awt.event.KeyEvent +import javax.swing.Box +import javax.swing.JComponent +import javax.swing.JPanel + +class CodeSuggestionDiffViewer( + request: DiffRequest, + private val mainEditor: Editor, + private val isManuallyOpened: Boolean +) : UnifiedDiffViewer(MyDiffContext(mainEditor.project), request), Disposable { + + private val popup: JBPopup = createSuggestionDiffPopup(component) + private val keyListener: KeyAdapter + private val visibleAreaListener: VisibleAreaListener + private val initialDocumentText: String = mainEditor.document.text + + init { + keyListener = getKeyListener() + visibleAreaListener = getVisibleAreaListener() + setupDiffEditor() + mainEditor.contentComponent.addKeyListener(keyListener) + mainEditor.scrollingModel.addVisibleAreaListener(visibleAreaListener) + } + + override fun onDispose() { + popup.dispose() + mainEditor.putUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER, null) + mainEditor.contentComponent.removeKeyListener(keyListener) + mainEditor.scrollingModel.removeVisibleAreaListener(visibleAreaListener) + super.onDispose() + } + + override fun onAfterRediff() { + val change = diffChanges?.firstOrNull() ?: return + + editor.component.preferredSize = + Dimension( + mainEditor.component.width / 2, + (editor.lineHeight * change.getChangedLinesCount()) + ) + adjustPopupSize(popup, editor) + + val adjustedLocation = + getAdjustedPopupLocation( + popup, + mainEditor, + change.lineFragment.startOffset1 + ) + + if (popup.isVisible) { + adjustPopupSize(popup, editor) + popup.setLocation(adjustedLocation) + } else { + popup.showInScreenCoordinates(mainEditor.component, adjustedLocation) + } + + doScrollToFirstChange() + } + + fun applyChanges() { + val changes = diffChanges ?: emptyList() + val change = changes.firstOrNull() ?: return + + if (isStateIsOutOfDate) return + if (!isEditable(Side.LEFT, true)) return + + val document: Document = getDocument(Side.LEFT) + + DiffUtil.executeWriteCommand(document, project, null) { + replaceChange(change, Side.RIGHT) + moveCaretToChange(change, document) + scheduleRediff() + } + rediff(true) + + if (changes.size == 1) { + popup.dispose() + } + } + + fun isVisible(): Boolean { + return popup.isVisible + } + + private fun setupDiffEditor() { + editor.apply { + settings.apply { + additionalLinesCount = 0 + isFoldingOutlineShown = false + isCaretRowShown = false + isBlinkCaret = false + isDndEnabled = false + isIndentGuidesShown = false + } + gutterComponentEx.isVisible = false + gutterComponentEx.parent.isVisible = false + scrollPane.horizontalScrollBar.isOpaque = false + } + + setupStatusLabel() + } + + private fun getTagPanel(): JComponent { + val tagPanel = JPanel(FlowLayout(FlowLayout.LEADING, 0, 0)) + if (!isManuallyOpened) { + tagPanel.add( + TagComponent( + "Trigger manually: ${getShortcutText(TriggerCustomPredictionAction.ID)}" + ).apply { + font = JBUI.Fonts.smallFont() + } + ) + tagPanel.add(Box.createHorizontalStrut(6)) + } + tagPanel.add(TagComponent("Accept: ${getShortcutText(AcceptNextPredictionRevisionAction.ID)}").apply { + setListener({ _, _ -> + applyChanges() + popup.dispose() + }, component) + font = JBUI.Fonts.smallFont() + }) + return tagPanel + } + + private fun setupStatusLabel() { + (editor.scrollPane as JBScrollPane).statusComponent = BorderLayoutPanel() + .andTransparent() + .withBorder(JBUI.Borders.empty(4)) + .addToRight(getTagPanel()) + } + + private fun getKeyListener(): KeyAdapter { + return object : KeyAdapter() { + override fun keyReleased(e: KeyEvent) { + if (mainEditor.document.text != initialDocumentText) { + popup.setUiVisible(false) + onDispose() + } + } + } + } + + private fun getVisibleAreaListener(): VisibleAreaListener { + return object : VisibleAreaListener { + override fun visibleAreaChanged(event: VisibleAreaEvent) { + val change = diffChanges?.firstOrNull() ?: return + val adjustedLocation = getAdjustedPopupLocation( + popup, + mainEditor, + change.lineFragment.startOffset1 + ) + + if (popup.isVisible && !popup.isDisposed) { + adjustPopupSize(popup, editor) + popup.setLocation(adjustedLocation) + } + } + } + } + + private fun doScrollToFirstChange() { + val changes = myModel.diffChanges ?: return + + var targetChange = ScrollToPolicy.FIRST_CHANGE.select( + ContainerUtil.filter( + changes + ) { it: UnifiedDiffChange -> !it.isSkipped }) + if (targetChange == null) targetChange = ScrollToPolicy.FIRST_CHANGE.select(changes) + if (targetChange == null) return + + val pointToScroll = myEditor.offsetToXY(targetChange.lineFragment.startOffset1) + pointToScroll.y -= myEditor.lineHeight + DiffUtil.scrollToPoint(myEditor, pointToScroll, false) + } + + private fun moveCaretToChange(change: UnifiedDiffChange, document: Document) { + val changeEndOffset = change.lineFragment.endOffset2 + val previousChar = document.getText(TextRange(changeEndOffset - 1, changeEndOffset)) + val offset = if (previousChar == "\n") changeEndOffset - 1 else changeEndOffset + + mainEditor.caretModel.moveToOffset(offset) + + val offsetPosition = mainEditor.offsetToXY(offset) + val isOffsetVisible = mainEditor.scrollingModel.visibleArea.contains(offsetPosition) + if (!isOffsetVisible) { + DiffUtil.scrollToCaret(mainEditor, false) + } + } + + private class MyDiffContext(private val project: Project?) : DiffContext() { + private val mainUi get() = getUserData(COMBINED_DIFF_MAIN_UI) + + private val ownContext: UserDataHolder = UserDataHolderBase() + + override fun getProject() = project + override fun isFocusedInWindow(): Boolean = mainUi?.isFocusedInWindow() ?: false + override fun isWindowFocused(): Boolean = mainUi?.isWindowFocused() ?: false + override fun requestFocusInWindow() { + mainUi?.requestFocusInWindow() + } + + override fun getUserData(key: Key): T? { + return ownContext.getUserData(key) + } + + override fun putUserData(key: Key, value: T?) { + ownContext.putUserData(key, value) + } + } + + companion object { + + @RequiresEdt + fun displayInlineDiff( + editor: Editor, + nextRevision: String, + isManuallyOpened: Boolean = false + ) { + editor.getUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER)?.dispose() + editor.putUserData(CodeGPTKeys.REMAINING_EDITOR_COMPLETION, null) + InlineCompletionSession.getOrNull(editor)?.let { + if (it.isActive()) { + InlineCompletionContext.getOrNull(editor)?.clear() + } + } + + val diffRequest = createSimpleDiffRequest(editor, nextRevision) + val diffViewer = CodeSuggestionDiffViewer(diffRequest, editor, isManuallyOpened) + editor.putUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER, diffViewer) + diffViewer.rediff(true) + } + } +} + +fun createSimpleDiffRequest(editor: Editor, nextRevision: String): SimpleDiffRequest { + val project = editor.project + val virtualFile = editor.virtualFile + val tempDiffFile = LightVirtualFile(virtualFile.name, nextRevision) + val diffContentFactory = DiffContentFactory.getInstance() + return SimpleDiffRequest( + null, + diffContentFactory.create(project, virtualFile), + diffContentFactory.create(project, tempDiffFile), + null, + null + ) +} + +fun UnifiedDiffChange.getChangedLinesCount(): Int { + val insertedLines = insertedRange.end - insertedRange.start + val deletedLines = deletedRange.end - deletedRange.start + return deletedLines + insertedLines + 2 +} + +fun getAdjustedPopupLocation(popup: JBPopup, editor: Editor, changeOffset: Int): Point { + val pointInEditor = editor.offsetToXY(changeOffset) + val visibleArea = editor.scrollingModel.visibleArea + val editorLocationOnScreen = editor.component.locationOnScreen + val isOffsetVisible = visibleArea.contains(pointInEditor) + val popupY = if (isOffsetVisible) { + editorLocationOnScreen.y + pointInEditor.y - editor.scrollingModel.verticalScrollOffset + } else { + if (pointInEditor.y < visibleArea.y) { + editorLocationOnScreen.y + } else { + editorLocationOnScreen.y + visibleArea.height - popup.size.height + } + } + val popupX = editorLocationOnScreen.x + editor.component.width - popup.size.width + return Point(popupX, popupY - editor.lineHeight) +} + +fun adjustPopupSize(popup: JBPopup, editor: Editor) { + val newWidth = editor.component.preferredSize.width + val newHeight = editor.component.preferredSize.height + popup.size = Dimension(newWidth, newHeight) + popup.content.revalidate() + popup.content.repaint() +} + +fun getShortcutText(actionId: String): String { + return KeymapUtil.getFirstKeyboardShortcutText( + ActionManager.getInstance().getAction(actionId) + ) +} + +fun createSuggestionDiffPopup(content: JComponent): JBPopup { + return JBPopupFactory.getInstance().createComponentPopupBuilder(content, null) + .setNormalWindowLevel(true) + .setCancelOnClickOutside(false) + .setRequestFocus(false) + .setFocusable(true) + .setMovable(true) + .setResizable(true) + .setShowBorder(true) + .setCancelKeyEnabled(true) + .setCancelOnWindowDeactivation(false) + .setCancelOnOtherWindowOpen(false) + .createPopup() +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/predictions/PredictionService.kt b/src/main/kotlin/ee/carlrobert/codegpt/predictions/PredictionService.kt new file mode 100644 index 00000000..adc9ae7a --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/predictions/PredictionService.kt @@ -0,0 +1,130 @@ +package ee.carlrobert.codegpt.predictions + +import com.intellij.codeInsight.lookup.LookupEvent +import com.intellij.notification.NotificationType +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.application.runReadAction +import com.intellij.openapi.components.Service +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import ee.carlrobert.codegpt.CodeGPTKeys.IS_FETCHING_COMPLETION +import ee.carlrobert.codegpt.CodeGPTKeys.PENDING_PREDICTION_CALL +import ee.carlrobert.codegpt.codecompletions.CodeCompletionProgressNotifier +import ee.carlrobert.codegpt.completions.CompletionClientProvider +import ee.carlrobert.codegpt.conversations.ConversationsState +import ee.carlrobert.codegpt.settings.prompts.PromptsSettings +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.codegpt.util.EditorUtil +import ee.carlrobert.codegpt.util.GitUtil +import ee.carlrobert.llm.client.codegpt.request.prediction.AutocompletionPredictionRequest +import ee.carlrobert.llm.client.codegpt.request.prediction.DirectPredictionRequest +import ee.carlrobert.llm.client.codegpt.request.prediction.PredictionRequest +import ee.carlrobert.llm.client.codegpt.response.CodeGPTException +import ee.carlrobert.llm.client.codegpt.response.PredictionResponse +import ee.carlrobert.llm.client.openai.completion.request.OpenAIChatCompletionStandardMessage +import okhttp3.Request + +@Service +class PredictionService { + + fun displayDirectPrediction(editor: Editor) { + val request = CompletionClientProvider.getCodeGPTClient() + .buildDirectPredictionRequest(createDirectPredictionRequest(editor)) + displayInlineDiff(editor, request) + } + + fun displayAutocompletePrediction(editor: Editor, textToInsert: String, beforeApply: String) { + val request = CompletionClientProvider.getCodeGPTClient() + .buildAutocompletionPredictionRequest( + createAutocompletePredictionRequest(editor, textToInsert, beforeApply) + ) + displayInlineDiff(editor, request) + } + + fun displayLookupPrediction(editor: Editor, event: LookupEvent, beforeApply: String) { + val request = CompletionClientProvider.getCodeGPTClient() + .buildLookupPredictionRequest( + createAutocompletePredictionRequest(editor, event.item?.lookupString ?: "", beforeApply) + ) + displayInlineDiff(editor, request) + } + + private fun displayInlineDiff(editor: Editor, request: Request) { + val prediction = getPrediction(editor, request) + if (prediction != null && !prediction.nextRevision.isNullOrEmpty()) { + runInEdt { + CodeSuggestionDiffViewer.displayInlineDiff(editor, prediction.nextRevision) + } + } + } + + private fun getPrediction(editor: Editor, request: Request): PredictionResponse? { + editor.project?.let { + CodeCompletionProgressNotifier.startLoading(it) + } + + val pendingCall = PENDING_PREDICTION_CALL.get(editor) + if (pendingCall != null) { + pendingCall.cancel() + return null + } + + try { + val client = CompletionClientProvider.getCodeGPTClient() + val call = client.createNewCall(request) + PENDING_PREDICTION_CALL.set(editor, call) + return client.getPrediction(call) + } catch (e: CodeGPTException) { + OverlayUtil.showNotification(e.detail, NotificationType.ERROR) + service().state.codeAssistantEnabled = false + return null + } catch (e: Exception) { + if (e.cause?.message != "Canceled") { + throw e + } + return null + } finally { + IS_FETCHING_COMPLETION.set(editor, false) + PENDING_PREDICTION_CALL.set(editor, null) + editor.project?.let { + CodeCompletionProgressNotifier.stopLoading(it) + } + } + } + + private fun createAutocompletePredictionRequest( + editor: Editor, + textToInsert: String, + beforeApply: String, + ): AutocompletionPredictionRequest { + val predictionRequest = AutocompletionPredictionRequest() + predictionRequest.appliedCompletion = textToInsert + predictionRequest.previousRevision = beforeApply + setDefaultParams(editor, predictionRequest) + return predictionRequest + } + + private fun createDirectPredictionRequest(editor: Editor): DirectPredictionRequest { + val predictionRequest = DirectPredictionRequest() + setDefaultParams(editor, predictionRequest) + return predictionRequest + } + + private fun setDefaultParams(editor: Editor, request: PredictionRequest) { + val messages: MutableList = mutableListOf() + ConversationsState.getInstance().currentConversation.messages.forEach { + messages.add(OpenAIChatCompletionStandardMessage("user", it.prompt)) + messages.add(OpenAIChatCompletionStandardMessage("assistant", it.response)) + } + request.apply { + currentRevision = runReadAction { editor.document.text } + customPrompt = + service().state.coreActions.codeAssistant.instructions + cursorOffset = runReadAction { editor.caretModel.offset } + gitChanges = GitUtil.getCurrentChanges(editor.project!!) + openFiles = EditorUtil.getOpenFiles(editor.project!!) + conversationMessages = messages.toList() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt new file mode 100644 index 00000000..c143ba04 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/predictions/TriggerCustomPredictionAction.kt @@ -0,0 +1,68 @@ +package ee.carlrobert.codegpt.predictions + +import com.intellij.codeInsight.hint.HintManagerImpl +import com.intellij.notification.NotificationType +import com.intellij.openapi.actionSystem.AnAction +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.components.service +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 ee.carlrobert.codegpt.CodeGPTKeys +import ee.carlrobert.codegpt.EncodingManager +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings +import ee.carlrobert.codegpt.ui.OverlayUtil + +class TriggerCustomPredictionAction : EditorAction(Handler()), HintManagerImpl.ActionToIgnore { + + companion object { + const val ID = "codegpt.triggerCustomPrediction" + } + + private class Handler : EditorWriteActionHandler() { + + override fun doExecute(editor: Editor, caret: Caret?, dataContext: DataContext?) { + if (GeneralSettings.getSelectedService() != ServiceType.CODEGPT) { + return + } + + if (!service().state.codeAssistantEnabled) { + val notification = OverlayUtil.getDefaultNotification( + "Please enable Code Assistant before using this feature.", + NotificationType.WARNING, + ) + notification.addAction(object : AnAction("Enable Code Assistant") { + override fun actionPerformed(e: AnActionEvent) { + service().state.codeAssistantEnabled = true + notification.hideBalloon() + } + }) + OverlayUtil.notify(notification) + + return + } + + if (service().countTokens(editor.document.text) > 4098) { + OverlayUtil.showNotification("The file exceeds the token limit of 4,098.") + return + } + + ApplicationManager.getApplication().executeOnPooledThread { + service().displayDirectPrediction(editor) + } + } + + override fun isEnabledForCaret( + editor: Editor, + caret: Caret, + dataContext: DataContext + ): Boolean { + return editor.getUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER) == null + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/Placeholder.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/Placeholder.kt index 1ae05622..8a09deba 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/Placeholder.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/Placeholder.kt @@ -8,6 +8,15 @@ import java.time.LocalDate enum class Placeholder(val description: String, val code: String) { DATE_ISO_8601("Current date in ISO 8601 format, e.g. 2021-01-01.", "$" + "DATE_ISO_8601"), BRANCH_NAME("The name of the current branch.", "$" + "BRANCH_NAME"), + GIT_DIFF( + "The unified diff output showing uncommitted changes in the current Git working directory, including staged and unstaged modifications.", + "$" + "GIT_DIFF" + ), + OPEN_FILES( + "The complete source code contents of all files currently open in the IDE editor tabs, maintaining their formatting and structure.", + "$" + "OPEN_FILES" + ), + ACTIVE_CONVERSATION("The complete conversation history with the AI assistant, including the most recent response and any relevant context from the current interaction.", "$" + "ACTIVE_CONVERSATION"), PREFIX("Code before the cursor.", "$" + "PREFIX"), SUFFIX("Code after the cursor.", "$" + "SUFFIX"), FIM_PROMPT( diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/PromptsSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/PromptsSettings.kt index 3dac9e5e..16f919f0 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/PromptsSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/PromptsSettings.kt @@ -31,6 +31,8 @@ class PromptsSettingsState : BaseState() { class CoreActionsState : BaseState() { companion object { + val DEFAULT_CODE_ASSISTANT_PROMPT = + getResourceContent("/prompts/core/code-assistant.txt") val DEFAULT_EDIT_CODE_PROMPT = getResourceContent("/prompts/core/edit-code.txt") val DEFAULT_GENERATE_COMMIT_MESSAGE_PROMPT = getResourceContent("/prompts/core/generate-commit-message.txt") @@ -39,7 +41,11 @@ class CoreActionsState : BaseState() { val DEFAULT_FIX_COMPILE_ERRORS_PROMPT = getResourceContent("/prompts/core/fix-compile-errors.txt") } - + var codeAssistant by property(CoreActionPromptDetailsState().apply { + name = "Code Assistant" + code = "CODE_ASSISTANT" + instructions = DEFAULT_CODE_ASSISTANT_PROMPT + }) var editCode by property(CoreActionPromptDetailsState().apply { name = "Edit Code" code = "EDIT_CODE" @@ -166,6 +172,10 @@ abstract class PromptDetailsState : BaseState() { var instructions by string() } +class CodeAssistantPromptDetailsState : PromptDetailsState() { + var code by string() +} + class CoreActionPromptDetailsState : PromptDetailsState() { var code by string() } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsForm.kt index e3531b45..f1958782 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsForm.kt @@ -104,10 +104,11 @@ class PromptsForm { val coreActionsFormState = getFormState(coreActionsNode) settings.coreActions.apply { - editCode = coreActionsFormState[0].toState() - fixCompileErrors = coreActionsFormState[1].toState() - generateCommitMessage = coreActionsFormState[2].toState() - generateNameLookups = coreActionsFormState[3].toState() + codeAssistant = coreActionsFormState[0].toState() + editCode = coreActionsFormState[1].toState() + fixCompileErrors = coreActionsFormState[2].toState() + generateCommitMessage = coreActionsFormState[3].toState() + generateNameLookups = coreActionsFormState[4].toState() } settings.chatActions.prompts = getFormState(chatActionsNode) .map { it.toState() } @@ -159,6 +160,7 @@ class PromptsForm { val formState = getFormState(coreActionsNode) val stateActions = listOf( + settingsState.codeAssistant, settingsState.editCode, settingsState.fixCompileErrors, settingsState.generateCommitMessage, @@ -212,6 +214,7 @@ class PromptsForm { val settings = service().state listOf( + settings.coreActions.codeAssistant, settings.coreActions.editCode, settings.coreActions.fixCompileErrors, settings.coreActions.generateCommitMessage, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsFormUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsFormUtil.kt index 38d284e8..597d4785 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsFormUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/PromptsFormUtil.kt @@ -1,16 +1,22 @@ package ee.carlrobert.codegpt.settings.prompts.form import ee.carlrobert.codegpt.settings.prompts.ChatActionPromptDetailsState +import ee.carlrobert.codegpt.settings.prompts.CodeAssistantPromptDetailsState import ee.carlrobert.codegpt.settings.prompts.CoreActionPromptDetailsState import ee.carlrobert.codegpt.settings.prompts.PersonaPromptDetailsState -import ee.carlrobert.codegpt.settings.prompts.form.details.ChatActionPromptDetails -import ee.carlrobert.codegpt.settings.prompts.form.details.CoreActionPromptDetails -import ee.carlrobert.codegpt.settings.prompts.form.details.FormPromptDetails -import ee.carlrobert.codegpt.settings.prompts.form.details.PersonaPromptDetails +import ee.carlrobert.codegpt.settings.prompts.form.details.* import javax.swing.tree.DefaultMutableTreeNode object PromptsFormUtil { + fun CodeAssistantPromptDetails.toState(): CodeAssistantPromptDetailsState { + val state = CodeAssistantPromptDetailsState() + state.code = this.code + state.name = this.name + state.instructions = this.instructions + return state + } + fun CoreActionPromptDetails.toState(): CoreActionPromptDetailsState { val state = CoreActionPromptDetailsState() state.code = this.code diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/AbstractEditorPromptPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/AbstractEditorPromptPanel.kt index bbe77c1b..5c59abda 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/AbstractEditorPromptPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/AbstractEditorPromptPanel.kt @@ -14,6 +14,7 @@ import com.intellij.openapi.editor.markup.TextAttributes import com.intellij.openapi.util.TextRange import com.intellij.ui.JBColor import java.awt.Dimension +import javax.swing.JPanel abstract class AbstractEditorPromptPanel( private val details: FormPromptDetails, @@ -28,6 +29,8 @@ abstract class AbstractEditorPromptPanel( } } + abstract fun getPanel(): JPanel + private fun createEditor(): Editor { return service() .run { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/ChatActionsDetailsPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/ChatActionsDetailsPanel.kt index 71113ccd..8f8a194a 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/ChatActionsDetailsPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/ChatActionsDetailsPanel.kt @@ -74,7 +74,7 @@ class ChatActionsDetailsPanel : PromptDetailsPanel { service().state.chatActions.startInNewWindow ) - fun getPanel(): JPanel = panel { + override fun getPanel(): JPanel = panel { row { cell(BorderLayoutPanel().addToTop(editor.component)).align(Align.FILL) .resizableColumn() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/CoreActionsDetailsPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/CoreActionsDetailsPanel.kt index 019f15e1..f9604ba8 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/CoreActionsDetailsPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/CoreActionsDetailsPanel.kt @@ -8,12 +8,14 @@ import com.intellij.ui.dsl.builder.Align import com.intellij.ui.dsl.builder.panel import com.intellij.ui.layout.ComponentPredicate import com.intellij.util.ui.components.BorderLayoutPanel +import ee.carlrobert.codegpt.settings.Placeholder import ee.carlrobert.codegpt.settings.prompts.CommitMessageTemplate -import ee.carlrobert.codegpt.settings.prompts.* +import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_CODE_ASSISTANT_PROMPT import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_EDIT_CODE_PROMPT import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_FIX_COMPILE_ERRORS_PROMPT import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_GENERATE_COMMIT_MESSAGE_PROMPT import ee.carlrobert.codegpt.settings.prompts.CoreActionsState.Companion.DEFAULT_GENERATE_NAME_LOOKUPS_PROMPT +import ee.carlrobert.codegpt.settings.prompts.PromptsSettings import javax.swing.JComponent import javax.swing.JPanel @@ -25,6 +27,27 @@ class CoreActionsDetailsPanel : PromptDetailsPanel { override fun create(details: CoreActionPromptDetails): JComponent { val editorPanel = when (details.code) { + + "CODE_ASSISTANT" -> CoreActionEditorPanel( + details, + DEFAULT_CODE_ASSISTANT_PROMPT, + buildString { + append("

Template for generating code assistant messages. Use the following placeholders to insert dynamic values:

\n") + append( + "
    ${ + listOf( + Placeholder.GIT_DIFF, + Placeholder.OPEN_FILES, + Placeholder.ACTIVE_CONVERSATION, + ).joinToString("\n") { + "
  • ${it.name}: ${it.description}
  • " + } + }
\n" + ) + }, + listOf("{GIT_DIFF}", "{OPEN_FILES}", "{ACTIVE_CONVERSATION}") + ) + "EDIT_CODE" -> CoreActionEditorPanel( details, DEFAULT_EDIT_CODE_PROMPT, @@ -90,7 +113,7 @@ class CoreActionsDetailsPanel : PromptDetailsPanel { highlightedPlaceholders: List = emptyList() ) : AbstractEditorPromptPanel(details, highlightedPlaceholders) { - fun getPanel(): JPanel = panel { + override fun getPanel(): JPanel = panel { row { cell(BorderLayoutPanel().addToTop(editor.component)) .align(Align.FILL) @@ -112,7 +135,7 @@ class CoreActionsDetailsPanel : PromptDetailsPanel { override fun invoke(): Boolean = defaultInstructions != editor.document.text }) row { - comment(description, 60) + comment(description, 96) } } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/FormDetails.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/FormDetails.kt index 08717171..01832cee 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/FormDetails.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/FormDetails.kt @@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.settings.prompts.form.details import com.intellij.openapi.observable.properties.AtomicBooleanProperty import ee.carlrobert.codegpt.settings.prompts.ChatActionPromptDetailsState +import ee.carlrobert.codegpt.settings.prompts.CodeAssistantPromptDetailsState import ee.carlrobert.codegpt.settings.prompts.CoreActionPromptDetailsState import ee.carlrobert.codegpt.settings.prompts.PersonaPromptDetailsState import javax.swing.JComponent @@ -17,6 +18,18 @@ sealed class FormPromptDetails { abstract var instructions: String? } +data class CodeAssistantPromptDetails( + override var name: String?, + override var instructions: String?, + val code: String? +) : FormPromptDetails() { + constructor(state: CodeAssistantPromptDetailsState) : this( + name = state.name, + instructions = state.instructions, + code = state.code + ) +} + data class CoreActionPromptDetails( override var name: String?, override var instructions: String?, diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/PersonasDetailsPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/PersonasDetailsPanel.kt index 7b001eb2..2cbae81c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/PersonasDetailsPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/prompts/form/details/PersonasDetailsPanel.kt @@ -60,7 +60,7 @@ class PersonasDetailsPanel(onSelected: (PersonaPromptDetails) -> Unit) : PromptD isEnabled = details.id != 1L } - fun getPanel(): JPanel = panel { + override fun getPanel(): JPanel = panel { row { cell(BorderLayoutPanel().addToTop(editor.component)) .align(Align.FILL) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt index 93d96b59..ccd759d5 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceForm.kt @@ -32,6 +32,11 @@ class CodeGPTServiceForm { renderer = CustomComboBoxRenderer() } + private val codeAssistantEnabledCheckBox = JBCheckBox( + CodeGPTBundle.get("shared.enableCodeAssistant"), + service().state.codeAssistantEnabled + ) + private val codeCompletionsEnabledCheckBox = JBCheckBox( CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"), service().state.codeCompletionSettings.codeCompletionsEnabled @@ -68,6 +73,11 @@ class CodeGPTServiceForm { UIUtil.createComment("settingsConfigurable.service.codegpt.codeCompletionModel.comment") ) .addVerticalGap(4) + .addComponent(codeAssistantEnabledCheckBox) + .addComponent( + UIUtil.createComment("settingsConfigurable.service.codegpt.enableCodeAssistant.comment", 90) + ) + .addVerticalGap(4) .addComponent(codeCompletionsEnabledCheckBox) .addComponentFillVertically(JPanel(), 0) .panel @@ -77,12 +87,14 @@ class CodeGPTServiceForm { fun isModified() = service().state.run { (chatCompletionModelComboBox.selectedItem as CodeGPTModel).code != chatCompletionSettings.model || (codeCompletionModelComboBox.selectedItem as CodeGPTModel).code != codeCompletionSettings.model + || codeAssistantEnabledCheckBox.isSelected != codeAssistantEnabled || codeCompletionsEnabledCheckBox.isSelected != codeCompletionSettings.codeCompletionsEnabled || getApiKey() != getCredential(CODEGPT_API_KEY) } fun applyChanges() { service().state.run { + codeAssistantEnabled = codeAssistantEnabledCheckBox.isSelected chatCompletionSettings.model = (chatCompletionModelComboBox.selectedItem as CodeGPTModel).code codeCompletionSettings.codeCompletionsEnabled = @@ -95,6 +107,7 @@ class CodeGPTServiceForm { fun resetForm() { service().state.run { + codeAssistantEnabledCheckBox.isSelected = codeAssistantEnabled chatCompletionModelComboBox.selectedItem = chatCompletionSettings.model codeCompletionModelComboBox.selectedItem = codeCompletionSettings.model codeCompletionsEnabledCheckBox.isSelected = diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt index a35a7f23..adf53bd6 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/codegpt/CodeGPTServiceSettings.kt @@ -13,6 +13,7 @@ class CodeGPTServiceSettings : class CodeGPTServiceSettingsState : BaseState() { var chatCompletionSettings by property(CodeGPTServiceChatCompletionSettingsState()) var codeCompletionSettings by property(CodeGPTServiceCodeCompletionSettingsState()) + var codeAssistantEnabled by property(true) } class CodeGPTServiceChatCompletionSettingsState : BaseState() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt index 3b23e424..e03da37f 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt @@ -10,6 +10,8 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorFactory import com.intellij.openapi.editor.EditorKind +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileEditor.FileEditor import com.intellij.openapi.fileEditor.FileEditorManager import com.intellij.openapi.fileEditor.TextEditor import com.intellij.openapi.fileEditor.impl.FileEditorManagerImpl @@ -20,9 +22,9 @@ import com.intellij.psi.PsiDocumentManager import com.intellij.psi.codeStyle.CodeStyleManager import com.intellij.testFramework.LightVirtualFile import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.llm.client.codegpt.request.prediction.FileDetails import java.time.LocalDateTime import java.time.format.DateTimeFormatter -import kotlin.math.min object EditorUtil { @JvmStatic @@ -74,6 +76,29 @@ object EditorUtil { return FileEditorManager.getInstance(project)?.selectedTextEditor } + @JvmStatic + fun getOpenFiles(project: Project): List { + val fileDocumentManager = FileDocumentManager.getInstance() + return FileEditorManager.getInstance(project).openFiles + .mapNotNull { + runReadAction { + FileDetails().apply { + name = "" + content = fileDocumentManager.getDocument(it)?.text ?: "" + modificationStamp = it.modificationStamp + } + } + } + .filter { !it.content.isNullOrEmpty() } + .sortedBy { it.modificationStamp } + .toList() + } + + @JvmStatic + fun getAllEditors(project: Project): List { + return FileEditorManager.getInstance(project).allEditors.toList() + } + @JvmStatic fun getSelectedEditorSelectedText(project: Project): String? { val selectedEditor = getSelectedEditor(project) @@ -168,4 +193,4 @@ object EditorUtil { } } } -} +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt index ff852c6f..9178da0c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/GitUtil.kt @@ -2,9 +2,13 @@ package ee.carlrobert.codegpt.util import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.diff.impl.patch.IdeaTextPatchBuilder +import com.intellij.openapi.diff.impl.patch.UnifiedDiffWriter import com.intellij.openapi.project.Project import com.intellij.openapi.project.guessProjectDir import com.intellij.openapi.vcs.VcsException +import com.intellij.openapi.vcs.changes.ChangeListManager +import ee.carlrobert.codegpt.codecompletions.truncateText import git4idea.GitCommit import git4idea.commands.Git import git4idea.commands.GitCommand @@ -12,6 +16,7 @@ import git4idea.commands.GitLineHandler import git4idea.history.GitHistoryUtils import git4idea.repo.GitRepository import git4idea.repo.GitRepositoryManager +import java.io.StringWriter object GitUtil { @@ -25,6 +30,28 @@ object GitUtil { ?: repositoryManager.repositories.firstOrNull() } + @JvmStatic + fun getCurrentChanges(project: Project): String? { + return getProjectRepository(project)?.let { repository -> + try { + val repoRootPath = repository.root.toNioPath() + val changes = ChangeListManager.getInstance(project).allChanges + .sortedBy { it.virtualFile?.timeStamp } + val patches = IdeaTextPatchBuilder.buildPatch( + project, changes, repoRootPath, false, true + ) + val diffWriter = StringWriter() + UnifiedDiffWriter.write( + null, repoRootPath, patches, diffWriter, "\n\n", null, null + ) + diffWriter.toString().cleanDiff().truncateText(1024, false) + } catch (e: VcsException) { + logger.error("Failed to get git context", e) + null + } + } + } + @Throws(VcsException::class) fun getCommitsForHashes( project: Project, @@ -100,4 +127,16 @@ object GitUtil { !it.startsWith("commit ") } } + + private fun String.cleanDiff(showContext: Boolean = false): String = + lineSequence() + .filterNot { line -> + line.startsWith("index ") || + line.startsWith("diff --git") || + line.startsWith("---") || + line.startsWith("+++") || + line.startsWith("===") || + (!showContext && line.startsWith(" ")) + } + .joinToString("\n") } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index e8540da2..ff93752f 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -17,6 +17,8 @@ + @@ -91,6 +93,14 @@ + + + + + + + + + + + + + + + + + + + + @@ -252,6 +278,8 @@ + + diff --git a/src/main/resources/messages/codegpt.properties b/src/main/resources/messages/codegpt.properties index 13c828fd..3115cb9e 100644 --- a/src/main/resources/messages/codegpt.properties +++ b/src/main/resources/messages/codegpt.properties @@ -25,6 +25,12 @@ action.statusbar.enableCompletions.MainMenu.text=Enable Completions action.statusbar.disableCompletions.text=Disable Completions action.statusbar.disableCompletions.description=Disable Code Completions action.statusbar.disableCompletions.MainMenu.text=Disable Completions +action.statusbar.enableCodeAssistant.text=Enable Code Assistant +action.statusbar.enableCodeAssistant.description=Enable Code Assistant +action.statusbar.enableCodeAssistant.MainMenu.text=Enable Code Assistant +action.statusbar.disableCodeAssistant.text=Disable Code Assistant +action.statusbar.disableCodeAssistant.description=Disable Code Assistant +action.statusbar.disableCodeAssistant.MainMenu.text=Disable Code Assistant action.compareWithOriginal.title=Compare with Original action.applyDirectly.title=Auto Apply action.explainGitCommit.title=Explain Commit with CodeGPT @@ -36,6 +42,7 @@ settingsConfigurable.service.label=Selected provider: settingsConfigurable.service.codegpt.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.codegpt.chatCompletionModel.comment=Choose a model optimized for conversational interactions, including assistance with general queries and explanations. settingsConfigurable.service.codegpt.codeCompletionModel.comment=Choose a model tailored for code completion-related tasks. +settingsConfigurable.service.codegpt.enableCodeAssistant.comment=If checked, Code Assistant will suggest related code updates as you make changes. settingsConfigurable.service.custom.openai.apiKey.comment=A secret value stored in the system's Keychain or KeePass, depending on your OS. This approach is recommended over storing the secret in the header as plain text. settingsConfigurable.service.openai.apiKey.comment=You can find the API key in your User settings. settingsConfigurable.service.openai.customModel.label=Custom model: @@ -257,6 +264,7 @@ imageAccordion.title=Attached image shared.image=Image shared.chatCompletions=Chat Completions shared.codeCompletions=Code Completions +shared.enableCodeAssistant=Enable Code Assistant NEW codeCompletionsForm.enableFeatureText=Enable code completions codeCompletionsForm.parseResponseAsChatCompletions=Parse response as Chat Completions codeCompletionsForm.overrideFimTemplate.label=Use built-in FIM template diff --git a/src/main/resources/prompts/core/code-assistant.txt b/src/main/resources/prompts/core/code-assistant.txt new file mode 100644 index 00000000..5fc97a9d --- /dev/null +++ b/src/main/resources/prompts/core/code-assistant.txt @@ -0,0 +1,14 @@ +Recent changes made to the project: + +{GIT_DIFF} + + +Currently open files: + +{OPEN_FILES} + + +Previous chat history between the AI and user: + +{ACTIVE_CONVERSATION} + diff --git a/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt b/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt index 3f694d6b..dcdca4cf 100644 --- a/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt +++ b/src/test/kotlin/ee/carlrobert/codegpt/codecompletions/CodeCompletionServiceTest.kt @@ -271,6 +271,7 @@ class CodeCompletionServiceTest : IntegrationTest() { fun `test apply inline suggestions with initial following text`() { useCodeGPTService() + service().state.codeAssistantEnabled = false service().state.codeCompletionSettings.multiLineEnabled = false myFixture.configureByText( "CompletionTest.java",