feat: code assistant (#810)

* feat: code assistant implementation

* refactor: clean up
This commit is contained in:
Carl-Robert Linnupuu 2024-12-26 23:27:18 +00:00 committed by GitHub
parent d81648549c
commit 8d6ca73064
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 944 additions and 62 deletions

View file

@ -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<Boolean> IS_PROMPT_TEXT_FIELD_DOCUMENT =
Key.create("codegpt.isPromptTextFieldDocument");
public static final Key<CodeSuggestionDiffViewer> EDITOR_PREDICTION_DIFF_VIEWER =
Key.create("codegpt.editorPredictionDiffViewer");
public static final Key<Call> PENDING_PREDICTION_CALL =
Key.create("codegpt.editorPendingPredictionCall");
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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<CodeGPTServiceSettings>().state.codeAssistantEnabled
|| service<EncodingManager>().countTokens(editor.document.text) > 4098
) {
return
}
ApplicationManager.getApplication().executeOnPooledThread {
service<PredictionService>().displayLookupPrediction(
editor,
event,
beforeApply
)
}
}
})
}
}
}

View file

@ -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<CodeGPTServiceSettings>().state
settings.codeAssistantEnabled = enableFeatureAction
}
override fun update(e: AnActionEvent) {
val codeAssistantEnabled = service<CodeGPTServiceSettings>().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)

View file

@ -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<AnAction>, context: DataContext): List<AnAction> {
val editor = CommonDataKeys.EDITOR.getData(context) ?: return emptyList()
actions.filterIsInstance<TriggerCustomPredictionAction>().takeIf { it.isNotEmpty() }?.let { return it }
if (InlineCompletionContext.getOrNull(editor) == null) {
return emptyList()
}

View file

@ -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<CodeGPTServiceSettings>().state.codeAssistantEnabled
&& service<EncodingManager>().countTokens(editor.document.text) <= 4098) {
ApplicationManager.getApplication().executeOnPooledThread {
service<PredictionService>().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)

View file

@ -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)
}
}
}

View file

@ -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)

View file

@ -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<ConfigurationSettings>().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

View file

@ -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()
}
}
}

View file

@ -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 <T> getUserData(key: Key<T>): T? {
return ownContext.getUserData(key)
}
override fun <T> putUserData(key: Key<T>, 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()
}

View file

@ -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<CodeGPTServiceSettings>().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<OpenAIChatCompletionStandardMessage> = 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<PromptsSettings>().state.coreActions.codeAssistant.instructions
cursorOffset = runReadAction { editor.caretModel.offset }
gitChanges = GitUtil.getCurrentChanges(editor.project!!)
openFiles = EditorUtil.getOpenFiles(editor.project!!)
conversationMessages = messages.toList()
}
}
}

View file

@ -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<CodeGPTServiceSettings>().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<CodeGPTServiceSettings>().state.codeAssistantEnabled = true
notification.hideBalloon()
}
})
OverlayUtil.notify(notification)
return
}
if (service<EncodingManager>().countTokens(editor.document.text) > 4098) {
OverlayUtil.showNotification("The file exceeds the token limit of 4,098.")
return
}
ApplicationManager.getApplication().executeOnPooledThread {
service<PredictionService>().displayDirectPrediction(editor)
}
}
override fun isEnabledForCaret(
editor: Editor,
caret: Caret,
dataContext: DataContext
): Boolean {
return editor.getUserData(CodeGPTKeys.EDITOR_PREDICTION_DIFF_VIEWER) == null
}
}
}

View file

@ -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(

View file

@ -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()
}

View file

@ -104,10 +104,11 @@ class PromptsForm {
val coreActionsFormState = getFormState<CoreActionPromptDetails>(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<ChatActionPromptDetails>(chatActionsNode)
.map { it.toState() }
@ -159,6 +160,7 @@ class PromptsForm {
val formState = getFormState<CoreActionPromptDetails>(coreActionsNode)
val stateActions = listOf(
settingsState.codeAssistant,
settingsState.editCode,
settingsState.fixCompileErrors,
settingsState.generateCommitMessage,
@ -212,6 +214,7 @@ class PromptsForm {
val settings = service<PromptsSettings>().state
listOf(
settings.coreActions.codeAssistant,
settings.coreActions.editCode,
settings.coreActions.fixCompileErrors,
settings.coreActions.generateCommitMessage,

View file

@ -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

View file

@ -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<EditorFactory>()
.run {

View file

@ -74,7 +74,7 @@ class ChatActionsDetailsPanel : PromptDetailsPanel {
service<PromptsSettings>().state.chatActions.startInNewWindow
)
fun getPanel(): JPanel = panel {
override fun getPanel(): JPanel = panel {
row {
cell(BorderLayoutPanel().addToTop(editor.component)).align(Align.FILL)
.resizableColumn()

View file

@ -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("<p>Template for generating code assistant messages. Use the following placeholders to insert dynamic values:</p>\n")
append(
"<ul>${
listOf(
Placeholder.GIT_DIFF,
Placeholder.OPEN_FILES,
Placeholder.ACTIVE_CONVERSATION,
).joinToString("\n") {
"<li><strong>${it.name}</strong>: ${it.description}</li>"
}
}</ul>\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<String> = 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)
}
}
}

View file

@ -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?,

View file

@ -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)

View file

@ -32,6 +32,11 @@ class CodeGPTServiceForm {
renderer = CustomComboBoxRenderer()
}
private val codeAssistantEnabledCheckBox = JBCheckBox(
CodeGPTBundle.get("shared.enableCodeAssistant"),
service<CodeGPTServiceSettings>().state.codeAssistantEnabled
)
private val codeCompletionsEnabledCheckBox = JBCheckBox(
CodeGPTBundle.get("codeCompletionsForm.enableFeatureText"),
service<CodeGPTServiceSettings>().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<CodeGPTServiceSettings>().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<CodeGPTServiceSettings>().state.run {
codeAssistantEnabled = codeAssistantEnabledCheckBox.isSelected
chatCompletionSettings.model =
(chatCompletionModelComboBox.selectedItem as CodeGPTModel).code
codeCompletionSettings.codeCompletionsEnabled =
@ -95,6 +107,7 @@ class CodeGPTServiceForm {
fun resetForm() {
service<CodeGPTServiceSettings>().state.run {
codeAssistantEnabledCheckBox.isSelected = codeAssistantEnabled
chatCompletionModelComboBox.selectedItem = chatCompletionSettings.model
codeCompletionModelComboBox.selectedItem = codeCompletionSettings.model
codeCompletionsEnabledCheckBox.isSelected =

View file

@ -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() {

View file

@ -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<FileDetails> {
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<FileEditor> {
return FileEditorManager.getInstance(project).allEditors.toList()
}
@JvmStatic
fun getSelectedEditorSelectedText(project: Project): String? {
val selectedEditor = getSelectedEditor(project)
@ -168,4 +193,4 @@ object EditorUtil {
}
}
}
}
}

View file

@ -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")
}

View file

@ -17,6 +17,8 @@
<projectListeners>
<listener topic="com.intellij.codeInsight.lookup.LookupManagerListener"
class="ee.carlrobert.codegpt.refactorings.RenameCompletionLookupListener"/>
<listener topic="com.intellij.codeInsight.lookup.LookupManagerListener"
class="ee.carlrobert.codegpt.CodeGPTLookupListener"/>
<listener topic="com.intellij.openapi.wm.ex.ToolWindowManagerListener"
class="ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowListener"/>
</projectListeners>
@ -91,6 +93,14 @@
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
</action>
<action id="codegpt.acceptNextPrediction" class="ee.carlrobert.codegpt.predictions.AcceptNextPredictionRevisionAction">
<keyboard-shortcut first-keystroke="TAB" keymap="$default"/>
</action>
<action id="codegpt.triggerCustomPrediction" class="ee.carlrobert.codegpt.predictions.TriggerCustomPredictionAction">
<keyboard-shortcut first-keystroke="ctrl ENTER" keymap="$default"/>
</action>
<action
id="codegpt.acceptNextInlayWord"
text="Apply next word"
@ -230,6 +240,22 @@
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.enableCodeAssistant"
class="ee.carlrobert.codegpt.actions.EnableCodeAssistantAction">
<keyboard-shortcut first-keystroke="ctrl shift alt a" keymap="$default"/>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.disableCodeAssistant"
class="ee.carlrobert.codegpt.actions.DisableCodeAssistantAction">
<keyboard-shortcut first-keystroke="ctrl shift alt a" keymap="$default"/>
<override-text place="MainMenu"/>
<override-text place="popup" use-text-of-place="MainMenu"/>
</action>
<action
id="statusbar.startServer"
class="ee.carlrobert.codegpt.actions.StartServerAction">
@ -252,6 +278,8 @@
<separator/>
<reference id="statusbar.stopServer" />
<reference id="statusbar.startServer" />
<reference id="statusbar.disableCodeAssistant" />
<reference id="statusbar.enableCodeAssistant" />
<reference id="statusbar.disableCompletions" />
<reference id="statusbar.enableCompletions" />
</group>

View file

@ -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 <a href="https://codegpt.ee/account">User settings</a>.
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 <a href="https://platform.openai.com/account/api-keys">User settings</a>.
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=<html>Enable Code Assistant <small><strong><sup style="color: green;">NEW</sup></strong></small></html>
codeCompletionsForm.enableFeatureText=Enable code completions
codeCompletionsForm.parseResponseAsChatCompletions=Parse response as Chat Completions
codeCompletionsForm.overrideFimTemplate.label=Use built-in FIM template

View file

@ -0,0 +1,14 @@
Recent changes made to the project:
<git_diff>
{GIT_DIFF}
</git_diff>
Currently open files:
<open_files>
{OPEN_FILES}
</open_files>
Previous chat history between the AI and user:
<active_conversation_history>
{ACTIVE_CONVERSATION}
</active_conversation_history>

View file

@ -271,6 +271,7 @@ class CodeCompletionServiceTest : IntegrationTest() {
fun `test apply inline suggestions with initial following text`() {
useCodeGPTService()
service<CodeGPTServiceSettings>().state.codeAssistantEnabled = false
service<ConfigurationSettings>().state.codeCompletionSettings.multiLineEnabled = false
myFixture.configureByText(
"CompletionTest.java",