diff --git a/src/main/java/ee/carlrobert/codegpt/Icons.java b/src/main/java/ee/carlrobert/codegpt/Icons.java index fb680fac..ce9e6ed4 100644 --- a/src/main/java/ee/carlrobert/codegpt/Icons.java +++ b/src/main/java/ee/carlrobert/codegpt/Icons.java @@ -30,5 +30,9 @@ public final class Icons { IconLoader.getIcon("/icons/greenCheckmark.svg", Icons.class); public static final Icon SendToTheLeft = IconLoader.getIcon("/icons/sendToTheLeft.svg", Icons.class); + public static final Icon OpenNewTab = + IconLoader.getIcon("/icons/openNewTab.svg", Icons.class); + public static final Icon AddFile = + IconLoader.getIcon("/icons/addFile.svg", Icons.class); public static final Icon StatusBarCompletionInProgress = new AnimatedIcon.Default(); } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java index 924d24ec..d8bb3bc3 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/IncludeFilesInContextAction.java @@ -6,9 +6,9 @@ import static ee.carlrobert.codegpt.settings.IncludedFilesSettingsState.DEFAULT_ import static ee.carlrobert.codegpt.settings.IncludedFilesSettingsState.DEFAULT_REPEATABLE_CONTEXT; import static java.lang.String.format; +import com.intellij.icons.AllIcons; import com.intellij.openapi.actionSystem.AnAction; import com.intellij.openapi.actionSystem.AnActionEvent; -import com.intellij.openapi.actionSystem.CommonDataKeys; import com.intellij.openapi.actionSystem.DataContext; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.project.Project; @@ -26,11 +26,11 @@ import com.intellij.util.ui.UI.PanelFactory; import ee.carlrobert.codegpt.CodeGPTBundle; import ee.carlrobert.codegpt.CodeGPTKeys; import ee.carlrobert.codegpt.EncodingManager; +import ee.carlrobert.codegpt.Icons; import ee.carlrobert.codegpt.ReferencedFile; import ee.carlrobert.codegpt.settings.IncludedFilesSettings; import ee.carlrobert.codegpt.ui.UIUtil; import ee.carlrobert.codegpt.ui.checkbox.FileCheckboxTree; -import ee.carlrobert.codegpt.ui.checkbox.PsiElementCheckboxTree; import ee.carlrobert.codegpt.ui.checkbox.VirtualFileCheckboxTree; import ee.carlrobert.codegpt.util.file.FileUtil; import java.awt.Dimension; @@ -49,11 +49,7 @@ public class IncludeFilesInContextAction extends AnAction { private static final Logger LOG = Logger.getInstance(IncludeFilesInContextAction.class); public IncludeFilesInContextAction() { - this("action.includeFilesInContext.title"); - } - - public IncludeFilesInContextAction(String customTitleKey) { - super(CodeGPTBundle.get(customTitleKey)); + super(Icons.AddFile); } @Override diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskQuestionAction.java similarity index 92% rename from src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java rename to src/main/java/ee/carlrobert/codegpt/actions/editor/AskQuestionAction.java index 6d00fbd4..eeaa16c3 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/CustomPromptAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/AskQuestionAction.java @@ -2,7 +2,7 @@ package ee.carlrobert.codegpt.actions.editor; import static java.lang.String.format; -import com.intellij.icons.AllIcons; +import com.intellij.icons.ExpUiIcons; import com.intellij.openapi.editor.Editor; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; @@ -20,13 +20,12 @@ import javax.swing.JTextArea; import javax.swing.SwingUtilities; import org.jetbrains.annotations.Nullable; -public class CustomPromptAction extends BaseEditorAction { +public class AskQuestionAction extends BaseEditorAction { private static String previousUserPrompt = ""; - CustomPromptAction() { - super("Custom Prompt", "Custom prompt description", AllIcons.Actions.Run_anything); - EditorActionsUtil.registerAction(this); + AskQuestionAction() { + super(ExpUiIcons.General.QuestionMark); } @Override 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 8f075ae9..6fecdf5a 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/BaseEditorAction.java @@ -13,6 +13,10 @@ import org.jetbrains.annotations.Nullable; public abstract class BaseEditorAction extends AnAction { + BaseEditorAction(@Nullable Icon icon) { + super(icon); + } + BaseEditorAction( @Nullable @NlsActions.ActionText String text, @Nullable @NlsActions.ActionDescription String description, 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 58e3c60d..09684458 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/EditorActionsUtil.java @@ -43,14 +43,9 @@ public class EditorActionsUtil { public static void refreshActions() { AnAction actionGroup = - ActionManager.getInstance().getAction("action.editor.group.EditorActionGroup"); + ActionManager.getInstance().getAction("CodeGPT.MyEditorActionsGroup"); if (actionGroup instanceof DefaultActionGroup group) { group.removeAll(); - group.add(new AskAction()); - group.add(new EditCodeAction(Actions.EditSource)); - group.add(new CustomPromptAction()); - group.addSeparator(); - var configuredActions = ConfigurationSettings.getState().getTableData(); configuredActions.forEach((label, prompt) -> { // using label as action description to prevent com.intellij.diagnostic.PluginException @@ -77,8 +72,6 @@ public class EditorActionsUtil { }; group.add(action); }); - group.addSeparator(); - group.add(new IncludeFilesInContextAction("action.includeFileInContext.title")); } } diff --git a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java b/src/main/java/ee/carlrobert/codegpt/actions/editor/OpenNewChatAction.java similarity index 90% rename from src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java rename to src/main/java/ee/carlrobert/codegpt/actions/editor/OpenNewChatAction.java index b83f4b88..1a5c283d 100644 --- a/src/main/java/ee/carlrobert/codegpt/actions/editor/AskAction.java +++ b/src/main/java/ee/carlrobert/codegpt/actions/editor/OpenNewChatAction.java @@ -8,10 +8,10 @@ import ee.carlrobert.codegpt.conversations.ConversationsState; import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager; import org.jetbrains.annotations.NotNull; -public class AskAction extends AnAction { +public class OpenNewChatAction extends AnAction { - public AskAction() { - super("New Chat", "Chat with CodeGPT", Icons.Sparkle); + public OpenNewChatAction() { + super(Icons.OpenNewTab); EditorActionsUtil.registerAction(this); } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java index c1f265f0..0dcab1fa 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowContentManager.java @@ -50,6 +50,10 @@ public final class ChatToolWindowContentManager { .sendMessage(message, conversationType); } + public Optional tryFindActiveChatTabPanel() { + return tryFindChatTabbedPane().flatMap(ChatToolWindowTabbedPane::tryFindActiveTabPanel); + } + public void displayConversation(@NotNull Conversation conversation) { displayChatTab(); tryFindChatTabbedPane() 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 10895862..94e5d315 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/ChatToolWindowTabPanel.java @@ -7,6 +7,8 @@ import static java.lang.String.format; import com.intellij.openapi.Disposable; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; +import com.intellij.openapi.editor.Editor; +import com.intellij.openapi.editor.SelectionModel; import com.intellij.openapi.project.Project; import com.intellij.ui.JBColor; import com.intellij.util.ui.JBUI; @@ -30,6 +32,8 @@ import ee.carlrobert.codegpt.toolwindow.chat.ui.textarea.TotalTokensPanel; import ee.carlrobert.codegpt.toolwindow.ui.ChatToolWindowLandingPanel; import ee.carlrobert.codegpt.ui.OverlayUtil; import ee.carlrobert.codegpt.ui.textarea.AppliedActionInlay; +import ee.carlrobert.codegpt.ui.textarea.AppliedCodeActionInlay; +import ee.carlrobert.codegpt.ui.textarea.AppliedSuggestionActionInlay; import ee.carlrobert.codegpt.ui.textarea.UserInputPanel; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.CreateDocumentationActionItem; import ee.carlrobert.codegpt.ui.textarea.suggestion.item.DocumentationActionItem; @@ -266,7 +270,7 @@ public class ChatToolWindowTabPanel implements Disposable { requestHandler.call(callParameters); } - private Unit handleSubmit(String text, List appliedInlayActions) { + private Unit handleSubmit(String text, List appliedInlayActions) { var message = new Message(text); var editor = EditorUtil.getSelectedEditor(project); String highlightedText = null; @@ -281,28 +285,107 @@ public class ChatToolWindowTabPanel implements Disposable { } } message.setUserMessage(text); - message.setWebSearchIncluded(appliedInlayActions.stream() - .anyMatch(it -> it.getSuggestion() instanceof WebSearchActionItem)); + processAppliedInlayActions(message, appliedInlayActions, text, editor); + sendMessage(message, ConversationType.DEFAULT, highlightedText); + return Unit.INSTANCE; + } + private void processAppliedInlayActions( + Message message, + List appliedInlayActions, + String text, + Editor editor) { + for (var action : appliedInlayActions) { + if (action instanceof AppliedSuggestionActionInlay) { + processSuggestionActions( + message, + filterActions(appliedInlayActions, AppliedSuggestionActionInlay.class)); + } else if (action instanceof AppliedCodeActionInlay) { + processCodeActions( + message, + filterActions(appliedInlayActions, AppliedCodeActionInlay.class), + text, + editor); + } + } + } + + private List filterActions( + List actions, + Class actionClass) { + return actions.stream() + .filter(actionClass::isInstance) + .map(actionClass::cast) + .toList(); + } + + private boolean containsWebSearchActionInlay(List actions) { + return actions.stream().anyMatch(it -> it.getSuggestion() instanceof WebSearchActionItem); + } + + private void processSuggestionActions( + Message message, + List actions) { + message.setWebSearchIncluded(containsWebSearchActionInlay(actions)); + processDocumentationAction(message, actions); + processPersonaAction(message, actions); + } + + private void processDocumentationAction( + Message message, + List actions) { var addedDocumentation = CodeGPTKeys.ADDED_DOCUMENTATION.get(project); - var appliedInlayExists = appliedInlayActions.stream() - .anyMatch(it -> it.getSuggestion() instanceof DocumentationActionItem - || it.getSuggestion() instanceof CreateDocumentationActionItem); + var appliedInlayExists = actions.stream().anyMatch(it -> { + var suggestion = it.getSuggestion(); + return suggestion instanceof DocumentationActionItem + || suggestion instanceof CreateDocumentationActionItem; + }); + if (addedDocumentation != null && appliedInlayExists) { message.setDocumentationDetails(addedDocumentation); CodeGPTKeys.ADDED_DOCUMENTATION.set(project, null); } + } + private void processPersonaAction(Message message, List actions) { var addedPersona = CodeGPTKeys.ADDED_PERSONA.get(project); - var personaInlayExists = appliedInlayActions.stream() + var personaInlayExists = actions.stream() .anyMatch(it -> it.getSuggestion() instanceof PersonaActionItem); + if (addedPersona != null && personaInlayExists) { message.setPersonaDetails(addedPersona); CodeGPTKeys.ADDED_PERSONA.set(project, null); } + } - sendMessage(message, ConversationType.DEFAULT, highlightedText); - return Unit.INSTANCE; + private void processCodeActions(Message message, List actions, + String text, Editor editor) { + var stringBuilder = new StringBuilder(text); + var resultStringBuilder = new StringBuilder(); + int lastProcessedIndex = 0; + + for (var actionInlay : actions) { + var inlayOffset = actionInlay.getInlay().getOffset(); + var fileExtension = FileUtil.getFileExtension(editor.getVirtualFile().getName()); + + resultStringBuilder + .append(stringBuilder, lastProcessedIndex, Math.min(stringBuilder.length(), inlayOffset)) + .append('\n') + .append(formatCodeBlock(fileExtension, actionInlay.getCode())) + .append('\n'); + + lastProcessedIndex = inlayOffset; + } + + resultStringBuilder.append(stringBuilder, lastProcessedIndex, stringBuilder.length()); + + var result = resultStringBuilder.toString(); + message.setUserMessage(result); + message.setPrompt(result); + } + + private String formatCodeBlock(String fileExtension, String code) { + return String.format("```%s\n%s\n```", fileExtension, code); } private Unit handleCancel() { @@ -375,4 +458,8 @@ public class ChatToolWindowTabPanel implements Disposable { rootPanel.add(createUserPromptPanel(), BorderLayout.SOUTH); return rootPanel; } + + public void addSelection(String fileName, SelectionModel selectionModel) { + userInputPanel.addSelection(fileName, selectionModel); + } } diff --git a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java index f259ddf0..6e7643e4 100644 --- a/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java +++ b/src/main/java/ee/carlrobert/codegpt/toolwindow/chat/editor/ResponseEditorPanel.java @@ -133,19 +133,21 @@ public class ResponseEditorPanel extends JPanel implements Disposable { editorEx.setVerticalScrollbarVisible(false); editorEx.getContentComponent().setBorder(JBUI.Borders.emptyLeft(4)); editorEx.setBorder(IdeBorderFactory.createBorder(ColorUtil.fromHex("#48494b"))); - editorEx.setPermanentHeaderComponent(createHeaderComponent(editorEx, extension)); + editorEx.setPermanentHeaderComponent(createHeaderComponent(editorEx, extension, readOnly)); editorEx.setHeaderComponent(null); } - private JPanel createHeaderComponent(EditorEx editorEx, String extension) { + private JPanel createHeaderComponent(EditorEx editorEx, String extension, boolean readOnly) { var headerComponent = new JPanel(new BorderLayout()); headerComponent.setBorder( JBUI.Borders.compound( JBUI.Borders.customLine(ColorUtil.fromHex("#48494b"), 1, 1, 0, 1), JBUI.Borders.empty(4))); headerComponent.add(createExpandLink(editorEx), BorderLayout.LINE_START); - headerComponent.add(createHeaderActions(extension, editorEx).getComponent(), - BorderLayout.LINE_END); + if (!readOnly) { + headerComponent.add( + createHeaderActions(extension, editorEx).getComponent(), BorderLayout.LINE_END); + } return headerComponent; } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AddSelectionToContextAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AddSelectionToContextAction.kt new file mode 100644 index 00000000..46342309 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/AddSelectionToContextAction.kt @@ -0,0 +1,18 @@ +package ee.carlrobert.codegpt.actions.editor + +import com.intellij.icons.AllIcons +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.project.Project +import ee.carlrobert.codegpt.toolwindow.chat.ChatToolWindowContentManager + +class AddSelectionToContextAction : BaseEditorAction(AllIcons.General.Add) { + + override fun actionPerformed(project: Project, editor: Editor, selectedText: String) { + val chatToolWindowContentManager = project.service() + val chatTabPanel = chatToolWindowContentManager + .tryFindActiveChatTabPanel() + .orElseThrow() + chatTabPanel.addSelection(editor.virtualFile.name, editor.selectionModel) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt index 4c9220e7..acbfa330 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/actions/editor/EditCodeAction.kt @@ -1,36 +1,12 @@ package ee.carlrobert.codegpt.actions.editor -import com.intellij.openapi.actionSystem.CustomShortcutSet 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 java.awt.event.InputEvent -import java.awt.event.KeyEvent -import javax.swing.Icon -import javax.swing.KeyStroke - -class EditCodeAction : BaseEditorAction { - - constructor() : this(Icons.Sparkle) - - constructor(icon: Icon) : super( - "Edit Code", - "Allow LLM to edit code directly in your editor", - icon - ) { - registerCustomShortcutSet( - CustomShortcutSet( - KeyStroke.getKeyStroke( - KeyEvent.VK_E, - InputEvent.SHIFT_DOWN_MASK or InputEvent.META_DOWN_MASK - ) - ), null - ) - EditorActionsUtil.registerAction(this) - } +class EditCodeAction : BaseEditorAction(Icons.Sparkle) { override fun actionPerformed(project: Project, editor: Editor, selectedText: String) { runInEdt { EditCodePopover(editor).show() diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/CodePreviewTooltipContent.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/CodePreviewTooltipContent.kt new file mode 100644 index 00000000..bccfdac8 --- /dev/null +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/CodePreviewTooltipContent.kt @@ -0,0 +1,100 @@ +package ee.carlrobert.codegpt.ui.textarea + +import com.intellij.openapi.application.PathManager +import com.intellij.openapi.application.runUndoTransparentWriteAction +import com.intellij.openapi.components.service +import com.intellij.openapi.editor.Document +import com.intellij.openapi.editor.EditorFactory +import com.intellij.openapi.editor.colors.EditorColorsManager +import com.intellij.openapi.editor.ex.EditorEx +import com.intellij.openapi.fileEditor.FileDocumentManager +import com.intellij.openapi.fileTypes.FileTypeManager +import com.intellij.openapi.project.Project +import com.intellij.testFramework.LightVirtualFile +import com.intellij.ui.JBColor +import ee.carlrobert.codegpt.settings.configuration.ConfigurationSettings +import ee.carlrobert.codegpt.util.EditorUtil.reformatDocument +import java.awt.BorderLayout +import java.awt.Dimension +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import javax.swing.JComponent +import javax.swing.JPanel + +class CodePreviewTooltipContent( + private val project: Project, + fileName: String, + fileContent: String +) : JPanel() { + + private val fileType = service().getFileTypeByFileName(fileName) + private val editor: EditorEx + + init { + layout = BorderLayout() + background = JBColor.background() + + val document = createDocument(fileName, fileContent) + editor = createEditor(document) + + add(editor.component, BorderLayout.CENTER) + + // TODO + val minHeight = 80 + val contentHeight = calculateContentHeight(editor) + 56 // Popup header height? + val editorHeight = maxOf(minHeight, contentHeight) + + preferredSize = Dimension(400, editorHeight) + } + + fun getFocusableComponent(): JComponent { + return editor.component + } + + private fun createEditor(document: Document): EditorEx { + val editor = + (service() + .createEditor(document, project, fileType, true) as EditorEx) + .apply { + colorsScheme = EditorColorsManager.getInstance().schemeForCurrentUITheme + setVerticalScrollbarVisible(true) + setHorizontalScrollbarVisible(true) + settings.apply { + isLineNumbersShown = false + isLineMarkerAreaShown = false + isFoldingOutlineShown = false + additionalColumnsCount = 0 + additionalLinesCount = 0 + isRightMarginShown = false + isShowIntentionBulb = false + setGutterIconsShown(false) + } + } + + if (service().state.autoFormattingEnabled) { + runUndoTransparentWriteAction { + reformatDocument(project, document, 0, document.textLength) + } + } + + return editor + } + + private fun calculateContentHeight(editor: EditorEx): Int { + return editor.lineHeight * editor.document.lineCount + } + + fun dispose() { + service().releaseEditor(editor) + } + + private fun createDocument(fileName: String, fileContent: String): Document { + val timestamp = DateTimeFormatter.ofPattern("yyyyMMddHHmmss").format(LocalDateTime.now()) + val lightVirtualFile = LightVirtualFile( + "${PathManager.getTempPath()}/${timestamp}_$fileName", + fileContent + ) + val existingDocument = service().getDocument(lightVirtualFile) + return existingDocument ?: service().createDocument(fileContent) + } +} diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt index 7864b397..d2b1ffad 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextField.kt @@ -27,10 +27,19 @@ import java.awt.event.KeyEvent import java.awt.event.MouseEvent import java.util.* -data class AppliedActionInlay( - val suggestion: SuggestionItem, - val inlay: Inlay, -) +interface AppliedActionInlay { + val inlay: Inlay +} + +data class AppliedSuggestionActionInlay( + override val inlay: Inlay, + val suggestion: SuggestionItem?, +) : AppliedActionInlay + +data class AppliedCodeActionInlay( + override val inlay: Inlay, + val code: String +) : AppliedActionInlay const val AT_CHAR = '@' @@ -61,28 +70,66 @@ class PromptTextField( ) } - fun addInlayElement(actionPrefix: String, text: String?, actionItem: SuggestionActionItem) { + fun addInlayElement(actionPrefix: String, text: String?, actionItem: SuggestionActionItem?) { editor?.let { val startOffset = it.document.text.lastIndexOf(AT_CHAR) if (startOffset == -1) { throw IllegalStateException("No '@' symbol found in the text") } - runUndoTransparentWriteAction { - it.document.deleteString(startOffset, it.document.textLength) - it.document.setText(it.document.text + " ") - appliedInlays.add( - AppliedActionInlay( - actionItem, - it.inlayModel.addInlineElement( - startOffset, - true, - PromptTextFieldInlayRenderer(actionPrefix, text) { inlay -> - inlay.dispose() - })!!, - ) - ) - it.caretModel.moveToOffset(it.document.textLength) + addInlayElement(startOffset, actionPrefix, text, actionItem) + } + } + + fun addInlayElement( + actionPrefix: String, + text: String, + fileName: String? = null, + tooltipText: String? = null + ) { + editor?.let { + addInlayElement( + it.caretModel.offset, + actionPrefix, + text, + fileName = fileName, + tooltipText = tooltipText + ) + } + } + + private fun addInlayElement( + startOffset: Int, + actionPrefix: String, + text: String?, + actionItem: SuggestionActionItem? = null, + fileName: String? = null, + tooltipText: String? = null + ) { + runUndoTransparentWriteAction { + document.deleteString(startOffset, document.textLength) + document.setText(document.text + " ") + val inlay = editor?.inlayModel?.addInlineElement( + startOffset, + true, + PromptTextFieldInlayRenderer( + project, + actionPrefix, + text, + fileName ?: "", + tooltipText + ) { inlay -> + appliedInlays.removeIf { appliedInlay -> appliedInlay.inlay == inlay } + inlay.dispose() + }) + if (inlay != null) { + // TODO + if (tooltipText == null) { + appliedInlays.add(AppliedSuggestionActionInlay(inlay, actionItem)) + } else { + appliedInlays.add(AppliedCodeActionInlay(inlay, tooltipText)) + } + editor?.caretModel?.moveToOffset(document.textLength) } } } @@ -102,8 +149,10 @@ class PromptTextField( } private fun clear() { - runInEdt { text = "" } - clearInlays() + runInEdt { + text = "" + clearInlays() + } } private fun clearInlays() { diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt index 8d14e90a..7be2f76c 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/PromptTextFieldInlayRenderer.kt @@ -12,27 +12,39 @@ import com.intellij.openapi.editor.event.EditorMouseEvent import com.intellij.openapi.editor.event.EditorMouseListener import com.intellij.openapi.editor.event.EditorMouseMotionListener import com.intellij.openapi.editor.markup.TextAttributes +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.popup.JBPopup +import com.intellij.openapi.ui.popup.JBPopupFactory import com.intellij.ui.JBColor +import com.intellij.ui.awt.RelativePoint import com.intellij.util.ui.JBUI import java.awt.Cursor +import java.awt.Dimension import java.awt.Graphics2D +import java.awt.Point import java.awt.event.MouseEvent import java.awt.geom.Rectangle2D class PromptTextFieldInlayRenderer( + private val project: Project, private val actionPrefix: String, private val text: String?, + private val fileName: String, + private val tooltipText: String?, private val onClose: (Inlay<*>) -> Unit ) : EditorCustomElementRenderer { private val closeIcon = AllIcons.Actions.Close + private val helpIcon = AllIcons.General.ContextHelp + + private var tooltip: JBPopup? = null override fun calcWidthInPixels(inlay: Inlay<*>): Int { val editor = inlay.editor val font = editor.colorsScheme.getFont(EditorFontType.PLAIN) val textWidth = editor.component.getFontMetrics(font) .stringWidth(actionPrefix + (if (text != null) ":$text" else "")) - return textWidth + closeIcon.iconWidth + JBUI.scale(10) + return textWidth + closeIcon.iconWidth + JBUI.scale(10) + helpIcon.iconWidth + JBUI.scale(10) } override fun paint( @@ -51,6 +63,9 @@ class PromptTextFieldInlayRenderer( drawBorder(g, target) drawText(g, target, editor, currentTextAttributes) drawCloseIcon(g, target) + if (tooltipText != null) { + drawHelpIcon(g, target) + } addMouseListeners(editor, inlay, target) } @@ -98,20 +113,90 @@ class PromptTextFieldInlayRenderer( closeIcon.paintIcon(null, g, iconX, iconY) } + private fun drawHelpIcon(g: Graphics2D, target: Rectangle2D) { + val iconX = + (target.x + target.width - closeIcon.iconWidth - helpIcon.iconWidth - JBUI.scale(10)).toInt() + val iconY = (target.y + (target.height - helpIcon.iconHeight) / 2).toInt() + helpIcon.paintIcon(null, g, iconX, iconY) + } + + private fun showTooltip(inlay: Inlay<*>) { + if (tooltipText != null) { + hideTooltip() + + val tooltipContent = CodePreviewTooltipContent(project, fileName, tooltipText) + tooltip = JBPopupFactory.getInstance() + .createComponentPopupBuilder( + tooltipContent, + tooltipContent.getFocusableComponent() + ) + .setTitle("Code Preview") + .setResizable(true) + .setMovable(true) + .setStretchToOwnerHeight(true) + .setStretchToOwnerWidth(true) + .setMinSize( + Dimension( + tooltipContent.preferredSize?.width ?: 240, + (tooltipContent.preferredSize?.height ?: 0) + ) + ) + .createPopup() + tooltip?.show( + RelativePoint( + inlay.editor.contentComponent, + calculatePopupPoint(inlay, tooltipContent) + ) + ) + } + } + + private fun calculatePopupPoint( + inlay: Inlay<*>, + tooltipContent: CodePreviewTooltipContent + ): Point { + val visibleArea = inlay.editor.scrollingModel.visibleArea + val inlayBounds = inlay.bounds + if (inlayBounds != null) { + val x = inlayBounds.x + val tooltipHeight = tooltipContent.preferredSize?.height ?: 0 + val y = inlayBounds.y - tooltipHeight + return Point(x, y) + } + return Point(visibleArea.x, visibleArea.y) + } + + private fun hideTooltip() { + tooltip?.dispose() + } + private fun addMouseListeners(editor: Editor, inlay: Inlay<*>, target: Rectangle2D) { - fun isWithinIconBounds(e: MouseEvent): Boolean { - val iconX = (target.x + target.width - closeIcon.iconWidth - JBUI.scale(5)).toInt() - val iconY = (target.y + (target.height - closeIcon.iconHeight) / 2).toInt() - return e.x >= iconX && e.x <= iconX + closeIcon.iconWidth && - e.y >= iconY && e.y <= iconY + closeIcon.iconHeight + fun isWithinIconBounds(e: MouseEvent, icon: javax.swing.Icon, offsetX: Int): Boolean { + val iconX = when (icon) { + closeIcon -> (target.x + target.width - closeIcon.iconWidth - JBUI.scale(5)).toInt() + helpIcon -> (target.x + target.width - closeIcon.iconWidth - helpIcon.iconWidth - JBUI.scale( + 10 + )).toInt() + + else -> return false + } + val iconY = (target.y + (target.height - icon.iconHeight) / 2).toInt() + return e.x >= iconX && e.x <= iconX + icon.iconWidth && + e.y >= iconY && e.y <= iconY + icon.iconHeight } - fun updateCursor(event: MouseEvent, inlay: Inlay<*>) { + fun updateCursor(event: EditorMouseEvent, inlay: Inlay<*>) { editor.contentComponent.let { if (inlay.isValid) { val inlayBounds = inlay.bounds - if (inlayBounds != null && inlayBounds.contains(event.x, event.y)) { - it.cursor = if (isWithinIconBounds(event)) { + if (inlayBounds != null && inlayBounds.contains( + event.mouseEvent.x, + event.mouseEvent.y + ) + ) { + it.cursor = if (isWithinIconBounds(event.mouseEvent, closeIcon, 0) || + isWithinIconBounds(event.mouseEvent, helpIcon, 0) + ) { Cursor.getPredefinedCursor(Cursor.HAND_CURSOR) } else { Cursor.getDefaultCursor() @@ -125,15 +210,26 @@ class PromptTextFieldInlayRenderer( editor.addEditorMouseMotionListener(object : EditorMouseMotionListener { override fun mouseMoved(event: EditorMouseEvent) { - updateCursor(event.mouseEvent, inlay) + updateCursor(event, inlay) } }) editor.addEditorMouseListener(object : EditorMouseListener { override fun mouseClicked(event: EditorMouseEvent) { - if (isWithinIconBounds(event.mouseEvent)) { - onClose(inlay) - event.consume() + when { + isWithinIconBounds( + event.mouseEvent, + closeIcon, + JBUI.scale(helpIcon.iconWidth + 10) + ) -> { + onClose(inlay) + event.consume() + } + + isWithinIconBounds(event.mouseEvent, helpIcon, JBUI.scale(5)) -> { + showTooltip(inlay) + event.consume() + } } } }) diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt index 7a46c353..f28b5b07 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/UserInputPanel.kt @@ -5,6 +5,7 @@ import com.intellij.openapi.actionSystem.ActionPlaces import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.components.service +import com.intellij.openapi.editor.SelectionModel import com.intellij.openapi.observable.properties.AtomicBooleanProperty import com.intellij.openapi.project.Project import com.intellij.ui.components.AnActionLink @@ -178,4 +179,15 @@ class UserInputPanel( else -> false } } + + fun addSelection(fileName: String, selectionModel: SelectionModel) { + promptTextField.addInlayElement( + "code", + "$fileName (${selectionModel.selectionStartPosition?.line}:${selectionModel.selectionEndPosition?.line})", + fileName = fileName, + tooltipText = selectionModel.selectedText + ) + promptTextField.requestFocusInWindow() + selectionModel.removeSelection() + } } diff --git a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt index 01ee4645..8a0db5fc 100644 --- a/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt +++ b/src/main/kotlin/ee/carlrobert/codegpt/ui/textarea/suggestion/item/SuggestionActionItems.kt @@ -7,6 +7,7 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.vfs.VirtualFile import ee.carlrobert.codegpt.CodeGPTBundle import ee.carlrobert.codegpt.CodeGPTKeys +import ee.carlrobert.codegpt.Icons import ee.carlrobert.codegpt.settings.GeneralSettings import ee.carlrobert.codegpt.settings.documentation.DocumentationSettings import ee.carlrobert.codegpt.settings.documentation.DocumentationsConfigurable diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index 6e6d2e8d..2022bb18 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -101,6 +101,43 @@ class="com.intellij.openapi.actionSystem.DefaultActionGroup" popup="true" icon="ee.carlrobert.codegpt.Icons.DefaultSmall"> + + + + + + + + + + + + + + + + + + @@ -118,6 +155,7 @@ icon="ee.carlrobert.codegpt.Icons.DefaultSmall"> + text="CodeGPT: Edit Code" + class="ee.carlrobert.codegpt.actions.editor.EditCodeAction"> + + + + + + + + + diff --git a/src/main/resources/icons/addFile_dark.svg b/src/main/resources/icons/addFile_dark.svg new file mode 100644 index 00000000..843fd69c --- /dev/null +++ b/src/main/resources/icons/addFile_dark.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/openNewTab.svg b/src/main/resources/icons/openNewTab.svg new file mode 100644 index 00000000..432a9e51 --- /dev/null +++ b/src/main/resources/icons/openNewTab.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/main/resources/icons/openNewTab_dark.svg b/src/main/resources/icons/openNewTab_dark.svg new file mode 100644 index 00000000..cb15080c --- /dev/null +++ b/src/main/resources/icons/openNewTab_dark.svg @@ -0,0 +1,5 @@ + + + + +