diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java index 8b0dd460..8f075ae9 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java @@ -11,7 +11,7 @@ import javax.swing.Icon; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -abstract class BaseEditorAction extends AnAction { +public abstract class BaseEditorAction extends AnAction { BaseEditorAction( @Nullable @NlsActions.ActionText String text, diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java index 5d89428a..6d00fbd4 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java @@ -4,7 +4,6 @@ import static java.lang.String.format; import com.intellij.icons.AllIcons; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; import com.intellij.util.ui.FormBuilder; diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java index 39e5f8c6..ab199481 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -2,11 +2,11 @@ package ee.carlrobert.codegpt.actions.editor; import static java.lang.String.format; +import com.intellij.icons.AllIcons.Actions; import com.intellij.openapi.actionSystem.ActionManager; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.DefaultActionGroup; import com.intellij.openapi.editor.Editor; -import com.intellij.openapi.editor.impl.EditorImpl; import com.intellij.openapi.extensions.PluginId; import com.intellij.openapi.project.Project; import ee.carlrobert.codegpt.CodeGPTKeys; @@ -47,6 +47,7 @@ public class EditorActionsUtil { if (actionGroup instanceof DefaultActionGroup group) { group.removeAll(); group.add(new AskAction()); + group.add(new EditCodeAction(Actions.EditSource)); group.add(new CustomPromptAction()); group.addSeparator(); diff --git a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java index 22ff3a49..dcfe8e05 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/toolwindow/ReplaceCodeInMainEditorAction.java @@ -30,8 +30,8 @@ public class ReplaceCodeInMainEditorAction extends AnAction { var project = event.getProject(); var toolWindowEditor = event.getData(PlatformDataKeys.EDITOR); if (project != null && toolWindowEditor != null) { - EditorUtil.replaceMainEditorSelection( - project, + EditorUtil.replaceEditorSelection( + toolWindowEditor, requireNonNull(toolWindowEditor.getSelectionModel().getSelectedText())); } } diff --git a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java index 08734396..3b15e571 100644 --- a/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java +++ b/src/main/java/ee/carlrobert/codegpt/completions/CompletionRequestProvider.java @@ -122,6 +122,20 @@ public class CompletionRequestProvider { .build(); } + public static OpenAIChatCompletionRequest buildEditCodeRequest( + String context, + String model) { + return new OpenAIChatCompletionRequest.Builder( + List.of( + new OpenAIChatCompletionStandardMessage( + "system", + getResourceContent("/prompts/edit-code.txt")), + new OpenAIChatCompletionStandardMessage("user", context))) + .setModel(model) + .setStream(true) + .build(); + } + public static Request buildCustomOpenAICompletionRequest(String system, String context) { return buildCustomOpenAIChatCompletionRequest( ApplicationManager.getApplication().getService(CustomServiceSettings.class) diff --git a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java index 971d5cbf..3b64679a 100644 --- a/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java +++ b/src/main/java/ee/carlrobert/codegpt/settings/configuration/ConfigurationState.java @@ -11,7 +11,7 @@ public class ConfigurationState { private String systemPrompt = COMPLETION_SYSTEM_PROMPT; private String commitMessagePrompt = GENERATE_COMMIT_MESSAGE_SYSTEM_PROMPT; - private int maxTokens = 1000; + private int maxTokens = 2048; private double temperature = 0.1; private boolean checkForPluginUpdates = true; private boolean checkForNewScreenshots = false; diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java index 156c606a..a5088d94 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -44,6 +44,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; +import java.util.function.Consumer; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.SwingUtilities; @@ -274,7 +275,7 @@ public class ChatToolWindowTabPanel implements Disposable { panel.add(JBUI.Panels.simplePanel(createUserPromptTextAreaHeader( project, selectedService, - () -> { + (provider) -> { ConversationService.getInstance().startConversation(); contentManager.createNewTabPanel(); })), BorderLayout.NORTH); @@ -285,7 +286,7 @@ public class ChatToolWindowTabPanel implements Disposable { private JPanel createUserPromptTextAreaHeader( Project project, ServiceType selectedService, - Runnable onModelChange) { + Consumer onModelChange) { return JBUI.Panels.simplePanel() .withBorder(Borders.emptyBottom(8)) .andTransparent() diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java index ae8ccf00..64dcc594 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/actions/ReplaceSelectionAction.java @@ -27,7 +27,7 @@ public class ReplaceSelectionAction extends TrackableAction { public void handleAction(@NotNull AnActionEvent event) { var project = requireNonNull(event.getProject()); if (EditorUtil.isMainEditorTextSelected(project)) { - EditorUtil.replaceMainEditorSelection(project, editor.getDocument().getText()); + EditorUtil.replaceEditorSelection(editor, editor.getDocument().getText()); } else { OverlayUtil.showSelectedEditorSelectionWarning(event); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java index 1f40bad1..0671785e 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ui/textarea/ModelComboBoxAction.java @@ -1,7 +1,11 @@ package ee.carlrobert.codegpt.toolwindow.chat.ui.textarea; +import static ee.carlrobert.codegpt.settings.service.ServiceType.ANTHROPIC; +import static ee.carlrobert.codegpt.settings.service.ServiceType.AZURE; import static ee.carlrobert.codegpt.settings.service.ServiceType.CODEGPT; import static ee.carlrobert.codegpt.settings.service.ServiceType.CUSTOM_OPENAI; +import static ee.carlrobert.codegpt.settings.service.ServiceType.GOOGLE; +import static ee.carlrobert.codegpt.settings.service.ServiceType.LLAMA_CPP; import static ee.carlrobert.codegpt.settings.service.ServiceType.OLLAMA; import static ee.carlrobert.codegpt.settings.service.ServiceType.OPENAI; import static java.lang.String.format; @@ -28,20 +32,35 @@ import ee.carlrobert.codegpt.settings.service.llama.LlamaSettings; import ee.carlrobert.codegpt.settings.service.ollama.OllamaSettings; import ee.carlrobert.codegpt.settings.service.openai.OpenAISettings; import ee.carlrobert.llm.client.openai.completion.OpenAIChatCompletionModel; +import java.util.Arrays; import java.util.List; +import java.util.function.Consumer; import javax.swing.Icon; import javax.swing.JComponent; import org.jetbrains.annotations.NotNull; public class ModelComboBoxAction extends ComboBoxAction { - private final Runnable onModelChange; + private final Consumer onModelChange; private final Project project; + private final List availableProviders; - public ModelComboBoxAction(Project project, Runnable onModelChange, ServiceType selectedService) { + public ModelComboBoxAction( + Project project, + Consumer onModelChange, + ServiceType selectedService) { + this(project, onModelChange, selectedService, Arrays.asList(ServiceType.values())); + } + + public ModelComboBoxAction( + Project project, + Consumer onModelChange, + ServiceType selectedProvider, + List availableProviders) { this.project = project; this.onModelChange = onModelChange; - updateTemplatePresentation(selectedService); + this.availableProviders = availableProviders; + updateTemplatePresentation(selectedProvider); } public JComponent createCustomComponent(@NotNull String place) { @@ -70,52 +89,71 @@ public class ModelComboBoxAction extends ComboBoxAction { protected @NotNull DefaultActionGroup createPopupActionGroup(JComponent button) { var presentation = ((ComboBoxButton) button).getPresentation(); var actionGroup = new DefaultActionGroup(); - actionGroup.addSeparator("CodeGPT"); - actionGroup.addAll(getCodeGPTModelActions(project, presentation)); - actionGroup.addSeparator("OpenAI"); - List.of( - OpenAIChatCompletionModel.GPT_4_O, - OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW, - OpenAIChatCompletionModel.GPT_4_0125_128k, - OpenAIChatCompletionModel.GPT_3_5_0125_16k) - .forEach(model -> actionGroup.add(createOpenAIModelAction(model, presentation))); - actionGroup.addSeparator("Custom OpenAI"); - actionGroup.add(createModelAction( - CUSTOM_OPENAI, - ApplicationManager.getApplication().getService(CustomServiceSettings.class) - .getState() - .getTemplate() - .getProviderName(), - Icons.OpenAI, - presentation)); - actionGroup.addSeparator(); - actionGroup.add(createModelAction( - ServiceType.ANTHROPIC, - "Anthropic (Claude)", - Icons.Anthropic, - presentation)); - actionGroup.addSeparator(); - actionGroup.add( - createModelAction(ServiceType.AZURE, "Azure OpenAI", Icons.Azure, presentation)); - actionGroup.addSeparator(); - actionGroup.add(createModelAction( - ServiceType.LLAMA_CPP, - getLlamaCppPresentationText(), - Icons.Llama, - presentation)); - actionGroup.addSeparator("Ollama"); - ApplicationManager.getApplication() - .getService(OllamaSettings.class) - .getState() - .getAvailableModels() - .forEach(model -> - actionGroup.add(createOllamaModelAction(model, presentation))); - actionGroup.addSeparator(); - actionGroup.add(createModelAction( - ServiceType.GOOGLE, - "Google (Gemini)", - Icons.Google, - presentation)); + + if (availableProviders.contains(CODEGPT)) { + actionGroup.addSeparator("CodeGPT"); + actionGroup.addAll(getCodeGPTModelActions(project, presentation)); + } + if (availableProviders.contains(OPENAI)) { + actionGroup.addSeparator("OpenAI"); + List.of( + OpenAIChatCompletionModel.GPT_4_O, + OpenAIChatCompletionModel.GPT_4_VISION_PREVIEW, + OpenAIChatCompletionModel.GPT_4_0125_128k, + OpenAIChatCompletionModel.GPT_3_5_0125_16k) + .forEach(model -> actionGroup.add(createOpenAIModelAction(model, presentation))); + } + if (availableProviders.contains(CUSTOM_OPENAI)) { + actionGroup.addSeparator("Custom OpenAI"); + actionGroup.add(createModelAction( + CUSTOM_OPENAI, + ApplicationManager.getApplication().getService(CustomServiceSettings.class) + .getState() + .getTemplate() + .getProviderName(), + Icons.OpenAI, + presentation)); + } + if (availableProviders.contains(ANTHROPIC)) { + actionGroup.addSeparator("Anthropic"); + actionGroup.add(createModelAction( + ANTHROPIC, + "Anthropic (Claude)", + Icons.Anthropic, + presentation)); + } + if (availableProviders.contains(AZURE)) { + actionGroup.addSeparator("Azure"); + actionGroup.add( + createModelAction(AZURE, "Azure OpenAI", Icons.Azure, presentation)); + } + if (availableProviders.contains(GOOGLE)) { + actionGroup.addSeparator("Google"); + actionGroup.add(createModelAction( + GOOGLE, + "Google (Gemini)", + Icons.Google, + presentation)); + } + if (availableProviders.contains(LLAMA_CPP)) { + actionGroup.addSeparator("LLaMA C/C++"); + actionGroup.add(createModelAction( + LLAMA_CPP, + getLlamaCppPresentationText(), + Icons.Llama, + presentation)); + } + if (availableProviders.contains(OLLAMA)) { + actionGroup.addSeparator("Ollama"); + createOllamaModelAction("Default model", presentation); + ApplicationManager.getApplication() + .getService(OllamaSettings.class) + .getState() + .getAvailableModels() + .forEach(model -> + actionGroup.add(createOllamaModelAction(model, presentation))); + } + return actionGroup; } @@ -211,7 +249,7 @@ public class ModelComboBoxAction extends ComboBoxAction { @Override public void actionPerformed(@NotNull AnActionEvent e) { - handleModelChange(serviceType, label, icon, comboBoxPresentation); + handleModelChange(serviceType, label, label, icon, comboBoxPresentation); } @Override @@ -224,12 +262,13 @@ public class ModelComboBoxAction extends ComboBoxAction { private void handleModelChange( ServiceType serviceType, String label, + String code, Icon icon, Presentation comboBoxPresentation) { GeneralSettings.getCurrentState().setSelectedService(serviceType); comboBoxPresentation.setIcon(icon); comboBoxPresentation.setText(label); - onModelChange.run(); + onModelChange.accept(serviceType); } private AnAction createCodeGPTModelAction(CodeGPTModel model, Presentation comboBoxPresentation) { @@ -249,6 +288,7 @@ public class ModelComboBoxAction extends ComboBoxAction { handleModelChange( CODEGPT, model.getName(), + model.getCode(), model.getIcon(), comboBoxPresentation); } @@ -280,6 +320,7 @@ public class ModelComboBoxAction extends ComboBoxAction { handleModelChange( OLLAMA, model, + model, Icons.Ollama, comboBoxPresentation); } @@ -309,6 +350,7 @@ public class ModelComboBoxAction extends ComboBoxAction { handleModelChange( OPENAI, model.getDescription(), + model.getCode(), Icons.OpenAI, comboBoxPresentation); } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt new file mode 100644 index 00000000..803c565e --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt @@ -0,0 +1,27 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.Icons +import ee.carlrobert.codegpt.ui.EditCodePopover +import javax.swing.Icon + +class EditCodeAction : BaseEditorAction { + + constructor() : this(Icons.Sparkle) + + constructor(icon: Icon) : super( + "Edit Code", + "Allow LLM to edit code directly in your editor", + icon + ) { + EditorActionsUtil.registerAction(this) + } + + override fun actionPerformed(project: Project, editor: Editor, selectedText: String) { + runInEdt { + EditCodePopover(editor).show() + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeCompletionHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeCompletionHandler.kt new file mode 100644 index 00000000..50b12c34 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeCompletionHandler.kt @@ -0,0 +1,119 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.notification.NotificationType +import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.markup.* +import com.intellij.openapi.util.TextRange +import com.intellij.psi.PsiDocumentManager +import com.intellij.psi.codeStyle.CodeStyleManager +import com.intellij.ui.JBColor +import ee.carlrobert.codegpt.ui.ObservableProperties +import ee.carlrobert.codegpt.ui.OverlayUtil +import ee.carlrobert.llm.client.openai.completion.ErrorDetails +import ee.carlrobert.llm.completion.CompletionEventListener +import okhttp3.sse.EventSource + +class EditCodeCompletionHandler( + private val editor: Editor, + private val observableProperties: ObservableProperties, + private val selectionTextRange: TextRange +) : CompletionEventListener { + + private var replacedLength = 0 + private var currentHighlighter: RangeHighlighter? = null + + override fun onMessage(message: String, eventSource: EventSource) { + handleDiff(message) + } + + override fun onComplete(messageBuilder: StringBuilder) { + cleanupAndFormat() + observableProperties.loading.set(false) + } + + override fun onError(error: ErrorDetails, ex: Throwable) { + OverlayUtil.showNotification( + "Something went wrong while requesting completion. Please try again.", + NotificationType.ERROR + ) + } + + private fun highlightCurrentRow(editor: Editor) { + currentHighlighter?.let { editor.markupModel.removeHighlighter(it) } + + val document = editor.document + val lineNumber = document.getLineNumber(editor.caretModel.offset) + currentHighlighter = editor.markupModel.addRangeHighlighter( + document.getLineStartOffset(lineNumber), + document.getLineEndOffset(lineNumber), + HighlighterLayer.SELECTION - 1, + TextAttributes().apply { + effectType = EffectType.BOXED + effectColor = + JBColor.namedColor("PsiViewer.referenceHighlightColor", 0x6A7B15) + errorStripeColor = effectColor + }, + HighlighterTargetArea.EXACT_RANGE + ) + } + + + private fun handleDiff(message: String) { + runWriteCommandAction(editor.project) { + val document = editor.document + val startOffset = selectionTextRange.startOffset + val endOffset = selectionTextRange.endOffset + + val remainingOriginalLength = endOffset - (startOffset + replacedLength) + if (remainingOriginalLength > 0) { + document.replaceString( + startOffset + replacedLength, + startOffset + replacedLength + minOf( + message.length, + remainingOriginalLength + ), + message + ) + } else { + document.insertString(startOffset + replacedLength, message) + } + + replacedLength += message.length + editor.caretModel.moveToOffset(startOffset + replacedLength) + highlightCurrentRow(editor) + } + } + + private fun cleanupHighlighter() { + currentHighlighter?.let { editor.markupModel.removeHighlighter(it) } + currentHighlighter = null + } + + private fun cleanupAndFormat() { + val project = editor.project ?: return + runWriteCommandAction(project) { + val document = editor.document + val psiDocumentManager = project.service() + val psiFile = psiDocumentManager.getPsiFile(document) + ?: return@runWriteCommandAction + val startOffset = selectionTextRange.startOffset + val endOffset = selectionTextRange.endOffset + val newEndOffset = startOffset + replacedLength + + if (newEndOffset < endOffset) { + document.deleteString(newEndOffset, endOffset) + } + + psiDocumentManager.commitDocument(document) + project.service().reformatText( + psiFile, + listOf(TextRange(startOffset, newEndOffset)) + ) + editor.caretModel.moveToOffset(newEndOffset) + psiDocumentManager.doPostponedOperationsAndUnblockDocument(document) + cleanupHighlighter() + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeSubmissionHandler.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeSubmissionHandler.kt new file mode 100644 index 00000000..1eaf4316 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeSubmissionHandler.kt @@ -0,0 +1,68 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.openapi.application.readAction +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.command.WriteCommandAction.runWriteCommandAction +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.util.TextRange +import com.jetbrains.rd.util.AtomicReference +import ee.carlrobert.codegpt.completions.CompletionClientProvider +import ee.carlrobert.codegpt.completions.CompletionRequestProvider +import ee.carlrobert.codegpt.settings.service.codegpt.CodeGPTServiceSettings +import ee.carlrobert.codegpt.ui.ObservableProperties + +class EditCodeSubmissionHandler( + private val editor: Editor, + private val observableProperties: ObservableProperties, +) { + + private val previousSourceRef = AtomicReference(null) + + suspend fun handleSubmit(userPrompt: String) { + try { + observableProperties.loading.set(true) + observableProperties.submitted.set(true) + + previousSourceRef.getAndSet(editor.document.text) + val (selectionTextRange, selectedText) = readAction { + editor.selectionModel.run { + Pair( + TextRange(selectionStart, selectionEnd), + editor.selectionModel.selectedText ?: "" + ) + } + } + runInEdt { editor.selectionModel.removeSelection() } + + // TODO: Support other providers + CompletionClientProvider.getCodeGPTClient().getChatCompletionAsync( + CompletionRequestProvider.buildEditCodeRequest( + "$userPrompt\n\n$selectedText", + service().state.chatCompletionSettings.model + ), + EditCodeCompletionHandler(editor, observableProperties, selectionTextRange) + ) + } finally { + observableProperties.loading.set(false) + } + } + + fun handleAccept() { + observableProperties.accepted.set(true) + observableProperties.submitted.set(false) + } + + fun handleReject() { + val prevSource = previousSourceRef.get() + if (!observableProperties.accepted.get() && prevSource != null) { + revertAllChanges(prevSource) + } + } + + private fun revertAllChanges(prevSource: String) { + runWriteCommandAction(editor.project) { + editor.document.replaceString(0, editor.document.textLength, prevSource) + } + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt index 936eedc9..9f8d0385 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceChatCompletionForm.kt @@ -1,5 +1,6 @@ package ee.carlrobert.codegpt.settings.service.custom.form +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.ui.MessageType import com.intellij.util.ui.FormBuilder import ee.carlrobert.codegpt.CodeGPTBundle @@ -86,7 +87,7 @@ class CustomServiceChatCompletionForm( internal inner class TestConnectionEventListener : CompletionEventListener { override fun onMessage(value: String?, eventSource: EventSource) { if (!value.isNullOrEmpty()) { - SwingUtilities.invokeLater { + runInEdt { OverlayUtil.showBalloon( CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), MessageType.INFO, @@ -98,7 +99,7 @@ class CustomServiceChatCompletionForm( } override fun onError(error: ErrorDetails, ex: Throwable) { - SwingUtilities.invokeLater { + runInEdt { OverlayUtil.showBalloon( CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") + "\n\n" diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt index 541f905a..cf949c0d 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/custom/form/CustomServiceCodeCompletionForm.kt @@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.settings.service.custom.form import com.intellij.icons.AllIcons.General import com.intellij.ide.HelpTooltip +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.ui.ComboBox import com.intellij.openapi.ui.MessageType import com.intellij.openapi.ui.panel.ComponentPanelBuilder @@ -158,7 +159,7 @@ class CustomServiceCodeCompletionForm( internal inner class TestConnectionEventListener : CompletionEventListener { override fun onMessage(value: String?, eventSource: EventSource) { if (!value.isNullOrEmpty()) { - SwingUtilities.invokeLater { + runInEdt { OverlayUtil.showBalloon( CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionSuccess"), MessageType.INFO, @@ -170,7 +171,7 @@ class CustomServiceCodeCompletionForm( } override fun onError(error: ErrorDetails, ex: Throwable) { - SwingUtilities.invokeLater { + runInEdt { OverlayUtil.showBalloon( CodeGPTBundle.get("settingsConfigurable.service.custom.openai.connectionFailed") + "\n\n" diff --git a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt index f9ede74e..3631a059 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/settings/service/ollama/OllamaSettingsForm.kt @@ -2,6 +2,7 @@ package ee.carlrobert.codegpt.settings.service.ollama import com.intellij.notification.NotificationType import com.intellij.openapi.application.invokeLater +import com.intellij.openapi.application.runInEdt import com.intellij.openapi.components.service import com.intellij.openapi.diagnostic.thisLogger import com.intellij.openapi.observable.util.whenTextChangedFromUi @@ -152,7 +153,7 @@ class OllamaSettingsForm { .map { it.name } } service().state.availableModels = models.toMutableList() - invokeLater { + runInEdt { modelComboBox.apply { if (models.isNotEmpty()) { model = DefaultComboBoxModel(models.toTypedArray()) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/EditCodePopover.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/EditCodePopover.kt new file mode 100644 index 00000000..8191d412 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/EditCodePopover.kt @@ -0,0 +1,184 @@ +package ee.carlrobert.codegpt.ui + +import com.intellij.ide.IdeBundle +import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI +import com.intellij.openapi.actionSystem.ActionPlaces +import com.intellij.openapi.application.runInEdt +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.editor.event.SelectionEvent +import com.intellij.openapi.editor.event.SelectionListener +import com.intellij.openapi.observable.properties.AtomicBooleanProperty +import com.intellij.openapi.observable.properties.ObservableProperty +import com.intellij.openapi.observable.util.not +import com.intellij.openapi.ui.popup.JBPopupFactory +import com.intellij.openapi.ui.popup.util.MinimizeButton +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.AlignX +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.Row +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.layout.ComponentPredicate +import com.intellij.util.ui.AsyncProcessIcon +import com.intellij.util.ui.JBUI +import ee.carlrobert.codegpt.CodeGPTBundle +import ee.carlrobert.codegpt.actions.editor.EditCodeSubmissionHandler +import ee.carlrobert.codegpt.settings.GeneralSettings +import ee.carlrobert.codegpt.settings.service.ServiceType.CODEGPT +import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.ModelComboBoxAction +import ee.carlrobert.codegpt.util.ApplicationUtil +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.launch +import javax.swing.JButton +import javax.swing.JPanel +import javax.swing.event.DocumentEvent + +data class ObservableProperties( + val submitted: AtomicBooleanProperty = AtomicBooleanProperty(false), + val accepted: AtomicBooleanProperty = AtomicBooleanProperty(false), + val loading: AtomicBooleanProperty = AtomicBooleanProperty(false), +) + +class EditCodePopover(private val editor: Editor) { + + private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + private val observableProperties = ObservableProperties() + private val submissionHandler = EditCodeSubmissionHandler(editor, observableProperties) + private val promptTextField = JBTextField("", 40).apply { + emptyText.appendText(CodeGPTBundle.get("editCodePopover.textField.emptyText")) + } + private val popup = JBPopupFactory.getInstance() + .createComponentPopupBuilder( + createPopupPanel(), + promptTextField + ) + .setTitle(CodeGPTBundle.get("editCodePopover.title")) + .setMovable(true) + .setCancelKeyEnabled(true) + .setCancelOnClickOutside(false) + .setCancelOnWindowDeactivation(false) + .setRequestFocus(true) + .setCancelButton(MinimizeButton(IdeBundle.message("tooltip.hide"))) + .setCancelCallback { + submissionHandler.handleReject() + true + } + .createPopup() + + fun show() { + popup.showInBestPositionFor(editor) + } + + private fun createPopupPanel(): JPanel { + return panel { + row { + cell(promptTextField) + } + row { + comment(CodeGPTBundle.get("editCodePopover.textField.comment")) + } + row { + button( + CodeGPTBundle.get("editCodePopover.submitButton.title"), + observableProperties.submitted.not(), + ) + button( + CodeGPTBundle.get("editCodePopover.followUpButton.title"), + observableProperties.submitted, + ) + button(CodeGPTBundle.get("editCodePopover.acceptButton.title")) { + submissionHandler.handleAccept() + popup.cancel() + } + .visibleIf(observableProperties.submitted) + cell(AsyncProcessIcon("edit_code_spinner")).visibleIf(observableProperties.loading) + link(CodeGPTBundle.get("shared.discard")) { + submissionHandler.handleReject() + popup.cancel() + } + .align(AlignX.RIGHT) + .visibleIf(observableProperties.submitted) + } + separator() + row { + text(CodeGPTBundle.get("editCodePopover.cancel.helpText")) + .applyToComponent { + font = JBUI.Fonts.smallFont() + } + cell( + ModelComboBoxAction( + ApplicationUtil.findCurrentProject(), + {}, + GeneralSettings.getSelectedService(), + listOf(CODEGPT) + ) + .createCustomComponent(ActionPlaces.UNKNOWN) + ).align(AlignX.RIGHT) + } + }.apply { + border = JBUI.Borders.empty(8, 8, 2, 8) + } + } + + private fun Row.button(title: String, visibleIf: ObservableProperty): Cell { + val button = JButton(title).apply { + putClientProperty(DarculaButtonUI.DEFAULT_STYLE_KEY, true) + addActionListener { + serviceScope.launch { + submissionHandler.handleSubmit(promptTextField.text) + promptTextField.text = "" + promptTextField.emptyText.text = + CodeGPTBundle.get("editCodePopover.textField.followUp.emptyText") + } + } + } + return cell(button) + .visibleIf(visibleIf) + .enabledIf( + EnabledButtonComponentPredicate( + button, + editor, + promptTextField, + observableProperties + ) + ) + } + + private class EnabledButtonComponentPredicate( + private val button: JButton, + private val editor: Editor, + private val promptTextField: JBTextField, + private val observableProperties: ObservableProperties + ) : ComponentPredicate() { + override fun invoke(): Boolean { + if (!editor.selectionModel.hasSelection()) { + button.toolTipText = "Please select code to continue" + } + if (promptTextField.text.isEmpty()) { + button.toolTipText = "Please enter a prompt to continue" + } + + return editor.selectionModel.hasSelection() + && promptTextField.text.isNotEmpty() + && observableProperties.loading.get().not() + } + + override fun addListener(listener: (Boolean) -> Unit) { + promptTextField.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + runInEdt { listener(invoke()) } + } + }) + editor.selectionModel.addSelectionListener(object : SelectionListener { + override fun selectionChanged(e: SelectionEvent) { + runInEdt { listener(invoke()) } + } + }) + observableProperties.loading.afterSet { + runInEdt { listener(invoke()) } + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt index 71471f6b..1cc90a53 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/util/EditorUtil.kt @@ -3,6 +3,7 @@ package ee.carlrobert.codegpt.util import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.PathManager +import com.intellij.openapi.application.runUndoTransparentWriteAction import com.intellij.openapi.command.WriteCommandAction import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Editor @@ -21,132 +22,139 @@ import java.time.LocalDateTime import java.time.format.DateTimeFormatter object EditorUtil { - @JvmStatic - fun createEditor(project: Project, fileExtension: String, code: String): Editor { - val timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()) - val fileName = "temp_$timestamp$fileExtension" - val lightVirtualFile = LightVirtualFile( - String.format("%s/%s", PathManager.getTempPath(), fileName), - code - ) - val existingDocument = FileDocumentManager.getInstance().getDocument(lightVirtualFile) - val document = existingDocument ?: EditorFactory.getInstance().createDocument(code) + @JvmStatic + fun createEditor(project: Project, fileExtension: String, code: String): Editor { + val timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()) + val fileName = "temp_$timestamp$fileExtension" + val lightVirtualFile = LightVirtualFile( + String.format("%s/%s", PathManager.getTempPath(), fileName), + code + ) + val existingDocument = FileDocumentManager.getInstance().getDocument(lightVirtualFile) + val document = existingDocument ?: EditorFactory.getInstance().createDocument(code) - disableHighlighting(project, document) + disableHighlighting(project, document) - return EditorFactory.getInstance().createEditor( - document, - project, - lightVirtualFile, - true, - EditorKind.MAIN_EDITOR - ) - } - - @JvmStatic - fun updateEditorDocument(editor: Editor, content: String) { - val document = editor.document - val application = ApplicationManager.getApplication() - val updateDocumentRunnable = Runnable { - application.runWriteAction { - WriteCommandAction.runWriteCommandAction(editor.project) { - document.replaceString(0, document.textLength, content) - editor.component.repaint() - editor.component.revalidate() - } - } + return EditorFactory.getInstance().createEditor( + document, + project, + lightVirtualFile, + true, + EditorKind.MAIN_EDITOR + ) } - if (application.isUnitTestMode) { - application.invokeAndWait(updateDocumentRunnable) - } else { - application.invokeLater(updateDocumentRunnable) - } - } - - @JvmStatic - fun hasSelection(editor: Editor?): Boolean { - return editor?.selectionModel?.hasSelection() == true - } - - @JvmStatic - fun getSelectedEditor(project: Project): Editor? { - val editorManager = FileEditorManager.getInstance(project) - return editorManager?.selectedTextEditor - } - - @JvmStatic - fun getSelectedEditorSelectedText(project: Project): String? { - val selectedEditor = getSelectedEditor(project) - return selectedEditor?.selectionModel?.selectedText - } - - @JvmStatic - fun isSelectedEditor(editor: Editor): Boolean { - val project = editor.project - if (project != null && !project.isDisposed) { - val editorManager = FileEditorManager.getInstance(project) ?: return false - if (editorManager is FileEditorManagerImpl) { - return editor == editorManager.getSelectedTextEditor(true) - } - val current = editorManager.selectedEditor - return (current is TextEditor) && editor == current.editor - } - return false - } - - @JvmStatic - fun isMainEditorTextSelected(project: Project): Boolean { - return hasSelection(getSelectedEditor(project)) - } - - @JvmStatic - fun replaceMainEditorSelection(project: Project, text: String) { - val application = ApplicationManager.getApplication() - application.invokeLater { - application.runWriteAction { - WriteCommandAction.runWriteCommandAction(project) { - val editor = getSelectedEditor(project) - editor?.let { - val selectionModel = editor.selectionModel - val startOffset = selectionModel.selectionStart - val endOffset = selectionModel.selectionEnd - val document = editor.document - - document.replaceString(startOffset, endOffset, text) - - if (ConfigurationSettings.getCurrentState().isAutoFormattingEnabled) { - reformatDocument(project, document, startOffset, endOffset) + @JvmStatic + fun updateEditorDocument(editor: Editor, content: String) { + val document = editor.document + val application = ApplicationManager.getApplication() + val updateDocumentRunnable = Runnable { + application.runWriteAction { + WriteCommandAction.runWriteCommandAction(editor.project) { + document.replaceString(0, document.textLength, content) + editor.component.repaint() + editor.component.revalidate() + } } + } + if (application.isUnitTestMode) { + application.invokeAndWait(updateDocumentRunnable) + } else { + application.invokeLater(updateDocumentRunnable) + } + } + + @JvmStatic + fun hasSelection(editor: Editor?): Boolean { + return editor?.selectionModel?.hasSelection() == true + } + + @JvmStatic + fun getSelectedEditor(project: Project): Editor? { + val editorManager = FileEditorManager.getInstance(project) + return editorManager?.selectedTextEditor + } + + @JvmStatic + fun getSelectedEditorSelectedText(project: Project): String? { + val selectedEditor = getSelectedEditor(project) + return selectedEditor?.selectionModel?.selectedText + } + + @JvmStatic + fun isSelectedEditor(editor: Editor): Boolean { + val project = editor.project + if (project != null && !project.isDisposed) { + val editorManager = FileEditorManager.getInstance(project) ?: return false + if (editorManager is FileEditorManagerImpl) { + return editor == editorManager.getSelectedTextEditor(true) + } + val current = editorManager.selectedEditor + return (current is TextEditor) && editor == current.editor + } + return false + } + + @JvmStatic + fun isMainEditorTextSelected(project: Project): Boolean { + return hasSelection(getSelectedEditor(project)) + } + + @JvmStatic + fun replaceEditorSelection(editor: Editor, text: String) { + val selectionModel = editor.selectionModel + val startOffset = selectionModel.selectionStart + val endOffset = selectionModel.selectionEnd + + replaceTextAndReformat(editor, startOffset, endOffset, text).also { editor.contentComponent.requestFocus() selectionModel.removeSelection() - } } - } } - } - @JvmStatic - fun reformatDocument( - project: Project, - document: Document, - startOffset: Int, - endOffset: Int - ) { - val psiDocumentManager = PsiDocumentManager.getInstance(project) - psiDocumentManager.commitDocument(document) - val psiFile = psiDocumentManager.getPsiFile(document) - psiFile?.let { - CodeStyleManager.getInstance(project).reformatText(psiFile, startOffset, endOffset) + @JvmStatic + fun reformatDocument( + project: Project, + document: Document, + startOffset: Int, + endOffset: Int + ) { + val psiDocumentManager = PsiDocumentManager.getInstance(project) + psiDocumentManager.commitDocument(document) + psiDocumentManager.getPsiFile(document)?.let { file -> + CodeStyleManager.getInstance(project).reformatText(file, startOffset, endOffset) + } } - } - @JvmStatic - fun disableHighlighting(project: Project, document: Document) { - val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) - psiFile?.let { - DaemonCodeAnalyzer.getInstance(project).setHighlightingEnabled(psiFile, false) + @JvmStatic + fun disableHighlighting(project: Project, document: Document) { + val psiFile = PsiDocumentManager.getInstance(project).getPsiFile(document) + psiFile?.let { + DaemonCodeAnalyzer.getInstance(project).setHighlightingEnabled(psiFile, false) + } + } + + private fun replaceTextAndReformat( + editor: Editor, + startOffset: Int, + endOffset: Int, + newText: String + ) { + editor.project?.let { project -> + runUndoTransparentWriteAction { + val document = editor.document + document.replaceString(startOffset, endOffset, newText) + + if (ConfigurationSettings.getCurrentState().isAutoFormattingEnabled) { + reformatDocument( + project, + document, + startOffset, + endOffset + ) + } + } + } } - } } diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 1fb1901d..a9f6c5dc 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -105,6 +105,17 @@ + + + + + +