Change project's name, heavy refactoring

This commit is contained in:
Carl-Robert Linnupuu 2023-03-12 20:56:44 +00:00
parent c0cea7cc43
commit 483abe146b
80 changed files with 969 additions and 968 deletions

View file

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.codegpt.ide.toolwindow.ChatGptToolWindow">
<grid id="27dc6" binding="chatGptToolWindowContent" layout-manager="GridLayoutManager" row-count="3" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="530" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<scrollpane id="8c1de" class="ee.carlrobert.codegpt.ide.toolwindow.components.ScrollPane" binding="scrollPane" custom-create="true">
<constraints>
<grid row="0" column="0" row-span="1" col-span="3" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</scrollpane>
<scrollpane id="a135c" binding="textAreaScrollPane" custom-create="true">
<constraints>
<grid row="2" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="7" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="db87e" class="javax.swing.JTextArea" binding="textArea" custom-create="true">
<constraints/>
<properties>
<lineWrap value="false"/>
<rows value="2"/>
</properties>
</component>
</children>
</scrollpane>
<component id="2426b" class="ee.carlrobert.codegpt.ide.toolwindow.components.GenerateButton" binding="generateButton" custom-create="true">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Stop Generating"/>
</properties>
</component>
</children>
</grid>
</form>

View file

@ -0,0 +1,223 @@
package ee.carlrobert.codegpt.ide.toolwindow;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.createIconLabel;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.createTextArea;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.justifyLeft;
import static java.lang.String.format;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.codegpt.ide.conversations.Conversation;
import ee.carlrobert.codegpt.ide.conversations.ConversationsState;
import ee.carlrobert.codegpt.ide.settings.SettingsConfigurable;
import ee.carlrobert.codegpt.ide.settings.SettingsState;
import ee.carlrobert.codegpt.ide.toolwindow.components.GenerateButton;
import ee.carlrobert.codegpt.ide.toolwindow.components.LandingView;
import ee.carlrobert.codegpt.ide.toolwindow.components.ScrollPane;
import ee.carlrobert.codegpt.ide.toolwindow.components.SyntaxTextArea;
import ee.carlrobert.codegpt.ide.toolwindow.components.TextArea;
import icons.Icons;
import java.awt.Cursor;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.Icon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
public class ChatGptToolWindow {
private static final List<SyntaxTextArea> textAreas = new ArrayList<>();
private final Project project;
private final ToolWindow toolWindow;
private JPanel chatGptToolWindowContent;
private ScrollPane scrollPane;
private ScrollablePanel scrollablePanel;
private JTextArea textArea;
private JScrollPane textAreaScrollPane;
private GenerateButton generateButton;
private boolean isLandingViewVisible;
public ChatGptToolWindow(@NotNull Project project, @NotNull ToolWindow toolWindow) {
this.project = project;
this.toolWindow = toolWindow;
}
public JPanel getContent() {
return chatGptToolWindowContent;
}
public void show() {
toolWindow.show();
}
public void displayUserMessage(String userMessage) {
if (isLandingViewVisible || ConversationsState.getCurrentConversation() == null) {
clearWindow();
}
addIconLabel(AllIcons.General.User, "User:");
scrollablePanel.add(createTextArea(userMessage));
scrollablePanel.validate();
scrollablePanel.repaint();
}
public void displayLandingView() {
isLandingViewVisible = true;
clearWindow();
var landingView = new LandingView();
scrollablePanel.add(landingView.createImageIconPanel());
addSpacing(16);
landingView.getQuestionPanels().forEach(panel -> {
scrollablePanel.add(panel);
addSpacing(16);
});
scrollablePanel.validate();
scrollablePanel.repaint();
}
public void displayConversation(Conversation conversation) {
clearWindow();
conversation.getMessages().forEach(message -> {
displayUserMessage(message.getPrompt());
addIconLabel(Icons.DefaultImageIcon, "ChatGPT:");
var textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
textArea.setText(message.getResponse());
textArea.displayCopyButton();
textArea.hideCaret();
scrollablePanel.add(textArea);
textAreas.add(textArea);
});
scrollToBottom();
scrollablePanel.validate();
scrollablePanel.repaint();
}
public void displayResponse(String prompt) {
addIconLabel(Icons.DefaultImageIcon, "ChatGPT:");
var settings = SettingsState.getInstance();
if (settings.isGPTOptionSelected && settings.apiKey.isEmpty()) {
notifyMissingCredential(project, "API key not provided.");
} else if (settings.isChatGPTOptionSelected && settings.accessToken.isEmpty()) {
notifyMissingCredential(project, "Access token not provided.");
} else {
var textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
addTextArea(textArea);
}
}
public void clearWindow() {
isLandingViewVisible = false;
generateButton.setVisible(false);
scrollablePanel.removeAll();
textAreas.clear();
}
public void addSpacing(int height) {
scrollablePanel.add(Box.createVerticalStrut(height));
}
public void addIconLabel(Icon icon, String text) {
addSpacing(8);
scrollablePanel.add(justifyLeft(createIconLabel(icon, text)));
addSpacing(8);
}
public void notifyMissingCredential(Project project, String text) {
var label = new JLabel(format("<html>%s <font color='#589df6'><u>Open Settings</u></font> to set one.</html>", text));
label.setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
label.addMouseListener(new MouseAdapter() {
public void mouseClicked(MouseEvent e) {
ShowSettingsUtil.getInstance().showSettingsDialog(project, SettingsConfigurable.class);
}
});
scrollablePanel.add(justifyLeft(label));
}
public void displayGenerateButton(Runnable onClick) {
generateButton.setVisible(true);
generateButton.setMode(GenerateButton.Mode.STOP, onClick);
}
public void stopGenerating(SyntaxTextArea textArea, Runnable onRefresh) {
generateButton.setMode(GenerateButton.Mode.REFRESH, onRefresh);
textArea.displayCopyButton();
textArea.hideCaret();
scrollToBottom();
}
public void scrollToBottom() {
scrollPane.scrollToBottom();
}
public void addTextArea(SyntaxTextArea textArea) {
scrollablePanel.add(textArea);
textAreas.add(textArea);
}
public void changeStyle() {
for (var textArea : textAreas) {
textArea.changeStyleViaThemeXml();
}
}
private void handleSubmit() {
var searchText = textArea.getText();
displayUserMessage(searchText);
displayResponse()
project.getService(ToolWindowService.class).sendMessage(searchText, project);
textArea.setText("");
scrollToBottom();
}
private void createUIComponents() {
textAreaScrollPane = new JBScrollPane() {
public JScrollBar createVerticalScrollBar() {
JScrollBar verticalScrollBar = new JScrollPane.ScrollBar(1);
verticalScrollBar.setPreferredSize(new Dimension(0, 0));
return verticalScrollBar;
}
};
textAreaScrollPane.setBorder(null);
textAreaScrollPane.setViewportBorder(null);
textAreaScrollPane.setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(1, 0, 0, 0, JBColor.border()),
BorderFactory.createEmptyBorder(0, 5, 0, 10)));
textAreaScrollPane.setViewportView(textArea);
textArea = new TextArea(this::handleSubmit, textAreaScrollPane);
scrollablePanel = new ScrollablePanel();
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
scrollPane = new ScrollPane(scrollablePanel);
generateButton = new GenerateButton();
displayLandingView();
}
}

View file

@ -0,0 +1,4 @@
package ee.carlrobert.codegpt.ide.toolwindow;
public class ChatToolWindowContent {
}

View file

@ -0,0 +1,56 @@
package ee.carlrobert.codegpt.ide.toolwindow;
import static java.util.Objects.requireNonNull;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentFactory;
import com.intellij.ui.content.ContentManager;
import java.util.Arrays;
import java.util.Optional;
import javax.swing.JPanel;
import org.jetbrains.annotations.NotNull;
public class ContentManagerService {
private static ContentManagerService instance;
private final ContentManager contentManager;
private ContentManagerService(Project project) {
this.contentManager = requireNonNull(ToolWindowManager.getInstance(project).getToolWindow("CodeGPT")).getContentManager();
}
public static ContentManagerService getInstance(@NotNull Project project) {
if (instance == null) {
instance = new ContentManagerService(project);
}
return instance;
}
public void addContent(JPanel content, String displayName) {
contentManager.addContent(ApplicationManager.getApplication()
.getService(ContentFactory.class)
.createContent(content, displayName, false));
}
public void displayChatTab() {
tryFindChatTabContent().ifPresentOrElse(
contentManager::setSelectedContent,
() -> contentManager.setSelectedContent(requireNonNull(contentManager.getContent(0)))
);
}
public Optional<Content> tryFindChatTabContent() {
return Arrays.stream(contentManager.getContents())
.filter(content -> "Chat".equals(content.getTabName()))
.findFirst();
}
public boolean isChatTabSelected() {
return tryFindChatTabContent()
.filter(contentManager::isSelected)
.isPresent();
}
}

View file

@ -0,0 +1,44 @@
package ee.carlrobert.codegpt.ide.toolwindow;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
import com.intellij.ui.content.ContentManagerEvent;
import com.intellij.ui.content.ContentManagerListener;
import ee.carlrobert.codegpt.ide.conversations.ConversationsState;
import ee.carlrobert.codegpt.ide.toolwindow.conversations.ConversationsToolWindow;
import org.jetbrains.annotations.NotNull;
public class ProjectToolWindowFactory implements ToolWindowFactory, DumbAware {
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
var toolWindowService = project.getService(ToolWindowService.class);
var chatToolWindow = new ChatGptToolWindow(project, toolWindow);
var conversationsToolWindow = new ConversationsToolWindow(project);
toolWindowService.setChatToolWindow(chatToolWindow);
var contentManagerService = ContentManagerService.getInstance(project);
contentManagerService.addContent(chatToolWindow.getContent(), "Chat");
contentManagerService.addContent(conversationsToolWindow.getContent(), "Conversation History");
toolWindow.addContentManagerListener(new ContentManagerListener() {
public void selectionChanged(@NotNull ContentManagerEvent event) {
var content = event.getContent();
if ("Conversation History".equals(content.getTabName()) && content.isSelected()) {
conversationsToolWindow.refresh();
}
}
});
displayRecentConversationIfPresent(toolWindowService, contentManagerService.isChatTabSelected());
}
private void displayRecentConversationIfPresent(ToolWindowService toolWindowService, boolean isChatTabSelected) {
var conversation = ConversationsState.getCurrentConversation();
if (conversation != null) {
if (isChatTabSelected) {
toolWindowService.getChatToolWindow().displayConversation(conversation);
}
}
}
}

View file

@ -0,0 +1,97 @@
package ee.carlrobert.codegpt.ide.toolwindow;
import com.intellij.ide.ui.LafManager;
import com.intellij.ide.ui.LafManagerListener;
import com.intellij.openapi.project.Project;
import ee.carlrobert.codegpt.client.ClientFactory;
import ee.carlrobert.codegpt.ide.conversations.ConversationsState;
import ee.carlrobert.codegpt.ide.conversations.message.Message;
import ee.carlrobert.codegpt.ide.settings.SettingsState;
import ee.carlrobert.codegpt.ide.toolwindow.components.SyntaxTextArea;
import icons.Icons;
import java.util.List;
import javax.swing.SwingWorker;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
public class ToolWindowService implements LafManagerListener {
private ChatGptToolWindow chatToolWindow;
@Override
public void lookAndFeelChanged(@NotNull LafManager source) {
chatToolWindow.changeStyle();
}
public void setChatToolWindow(ChatGptToolWindow chatToolWindow) {
this.chatToolWindow = chatToolWindow;
}
public ChatGptToolWindow getChatToolWindow() {
return chatToolWindow;
}
public void startRequest(String prompt, SyntaxTextArea textArea, Project project) {
var client = new ClientFactory().getClient();
chatToolWindow.displayGenerateButton(client::cancelRequest);
var conversationMessage = new Message(prompt);
new SwingWorker<Void, String>() {
protected Void doInBackground() {
client.getCompletionsAsync(
prompt,
this::publish,
(completedConversation) -> {
ConversationsState.getInstance().saveConversation(completedConversation);
stopGenerating(prompt, textArea, project);
},
(errorMessage) -> {
var currentConversation = ConversationsState.getCurrentConversation();
if (currentConversation != null) {
conversationMessage.setResponse(errorMessage);
currentConversation.addMessage(conversationMessage);
ConversationsState.getInstance().saveConversation(currentConversation);
}
textArea.append(errorMessage);
stopGenerating(prompt, textArea, project);
});
return null;
}
protected void process(List<String> chunks) {
for (String text : chunks) {
try {
textArea.append(text);
conversationMessage.setResponse(textArea.getText());
chatToolWindow.scrollToBottom();
} catch (Exception e) {
textArea.append("Something went wrong. Please try again later.");
throw new RuntimeException(e);
}
}
}
}.execute();
}
public void sendMessage(String prompt, Project project) {
chatToolWindow.addIconLabel(Icons.DefaultImageIcon, "ChatGPT:");
var settings = SettingsState.getInstance();
if (settings.isGPTOptionSelected && settings.apiKey.isEmpty()) {
chatToolWindow.notifyMissingCredential(project, "API key not provided.");
} else if (settings.isChatGPTOptionSelected && settings.accessToken.isEmpty()) {
chatToolWindow.notifyMissingCredential(project, "Access token not provided.");
} else {
var textArea = new SyntaxTextArea(true, true, SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
chatToolWindow.addTextArea(textArea);
startRequest(prompt, textArea, project);
}
}
private void stopGenerating(String prompt, SyntaxTextArea textArea, Project project) {
chatToolWindow.stopGenerating(textArea, () -> {
sendMessage(prompt, project);
chatToolWindow.scrollToBottom();
});
}
}

View file

@ -0,0 +1,299 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import java.awt.*;
import javax.swing.*;
import javax.swing.border.*;
// https://tips4java.wordpress.com/2009/09/27/component-border/
/**
* The ComponentBorder class allows you to place a real component in
* the space reserved for painting the Border of a component.
*
* This class takes advantage of the knowledge that all Swing components are
* also Containers. By default the layout manager is null, so we should be
* able to place a child component anywhere in the parent component. In order
* to prevent the child component from painting over top of the parent
* component a Border is added to the parent componet such that the insets of
* the Border will reserve space for the child component to be painted without
* affecting the parent component.
*/
public class ComponentBorder implements Border
{
public enum Edge
{
TOP,
LEFT,
BOTTOM,
RIGHT;
}
public static final float LEADING = 0.0f;
public static final float CENTER = 0.5f;
public static final float TRAILING = 1.0f;
private JComponent parent;
private JComponent component;
private Edge edge;
private float alignment;
private int gap = 5;
private boolean adjustInsets = true;
private Insets borderInsets = new Insets(0, 0, 0, 0);
/**
* Convenience constructor that uses the default edge (Edge.RIGHT) and
* alignment (CENTER).
*
* @param component the component to be added in the Border area
*/
public ComponentBorder(JComponent component)
{
this(component, Edge.RIGHT);
}
/**
* Convenience constructor that uses the default alignment (CENTER).
*
* @param component the component to be added in the Border area
* @param edge a valid Edge enum of TOP, LEFT, BOTTOM, RIGHT
*/
public ComponentBorder(JComponent component, Edge edge)
{
this(component, edge, CENTER);
}
/**
* Main constructor to create a ComponentBorder.
*
* @param component the component to be added in the Border area
* @param edge a valid Edge enum of TOP, LEFT, BOTTOM, RIGHT
* @param alignment the alignment of the component along the
* specified Edge. Must be in the range 0 - 1.0.
*/
public ComponentBorder(JComponent component, Edge edge, float alignment )
{
this.component = component;
component.setSize( component.getPreferredSize() );
component.setCursor(Cursor.getDefaultCursor());
setEdge( edge );
setAlignment( alignment );
}
public boolean isAdjustInsets()
{
return adjustInsets;
}
public void setAdjustInsets(boolean adjustInsets)
{
this.adjustInsets = adjustInsets;
}
/**
* Get the component alignment along the Border Edge
*
* @return the alignment
*/
public float getAlignment()
{
return alignment;
}
/**
* Set the component alignment along the Border Edge
*
* @param alignment a value in the range 0 - 1.0. Standard values would be
* CENTER (default), LEFT and RIGHT.
*/
public void setAlignment(float alignment)
{
this.alignment = alignment > 1.0f ? 1.0f : alignment < 0.0f ? 0.0f : alignment;
}
/**
* Get the Edge the component is positioned along
*
* @return the Edge
*/
public Edge getEdge()
{
return edge;
}
/**
* Set the Edge the component is positioned along
*
* @param edge the Edge the component is position on.
*/
public void setEdge(Edge edge)
{
this.edge = edge;
}
/**
* Get the gap between the border component and the parent component
*
* @return the gap in pixels.
*/
public int getGap()
{
return gap;
}
/**
* Set the gap between the border component and the parent component
*
* @param gap the gap in pixels (default is 5)
*/
public void setGap(int gap)
{
this.gap = gap;
}
//
// Implement the Border interface
//
public Insets getBorderInsets(Component c)
{
return borderInsets;
}
public boolean isBorderOpaque()
{
return false;
}
/**
* In this case a real component is to be painted. Setting the location
* of the component will cause it to be painted at that location.
*/
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height)
{
float x2 = (width - component.getWidth()) * component.getAlignmentX() + x;
float y2 = (height - component.getHeight()) * component.getAlignmentY() + y;
component.setLocation((int)x2, (int)y2);
}
/*
* Install this Border on the specified component by replacing the
* existing Border with a CompoundBorder containing the original Border
* and our ComponentBorder
*
* This method should only be invoked once all the properties of this
* class have been set. Installing the Border more than once will cause
* unpredictable results.
*/
public void install(JComponent parent)
{
this.parent = parent;
determineInsetsAndAlignment();
// Add this Border to the parent
Border current = parent.getBorder();
if (current == null)
{
parent.setBorder(this);
}
else
{
CompoundBorder compound = new CompoundBorder(current, this);
parent.setBorder(compound);
}
// Add component to the parent
parent.add(component);
}
/**
* The insets need to be determined so they are included in the preferred
* size of the component the Border is attached to.
*
* The alignment of the component is determined here so it doesn't need
* to be recalculated every time the Border is painted.
*/
private void determineInsetsAndAlignment()
{
borderInsets = new Insets(0, 0, 0, 0);
// The insets will only be updated for the edge the component will be
// diplayed on.
//
// The X, Y alignment of the component is controlled by both the edge
// and alignment parameters
if (edge == Edge.TOP)
{
borderInsets.top = component.getPreferredSize().height + gap;
component.setAlignmentX(alignment);
component.setAlignmentY(0.0f);
}
else if (edge == Edge.BOTTOM)
{
borderInsets.bottom = component.getPreferredSize().height + gap;
component.setAlignmentX(alignment);
component.setAlignmentY(1.0f);
}
else if (edge == Edge.LEFT)
{
borderInsets.left = component.getPreferredSize().width + gap;
component.setAlignmentX(0.0f);
component.setAlignmentY(alignment);
}
else if (edge == Edge.RIGHT)
{
borderInsets.right = component.getPreferredSize().width + gap;
component.setAlignmentX(1.0f);
component.setAlignmentY(alignment);
}
if (adjustInsets)
adjustBorderInsets();
}
/*
* The complimentary edges of the Border may need to be adjusted to allow
* the component to fit completely in the bounds of the parent component.
*/
private void adjustBorderInsets()
{
Insets parentInsets = parent.getInsets();
// May need to adust the height of the parent component to fit
// the component in the Border
if (edge == Edge.RIGHT || edge == Edge.LEFT)
{
int parentHeight = parent.getPreferredSize().height - parentInsets.top - parentInsets.bottom;
int diff = component.getHeight() - parentHeight;
if (diff > 0)
{
int topDiff = (int)(diff * alignment);
int bottomDiff = diff - topDiff;
borderInsets.top += topDiff;
borderInsets.bottom += bottomDiff;
}
}
// May need to adust the width of the parent component to fit
// the component in the Border
if (edge == Edge.TOP || edge == Edge.BOTTOM)
{
int parentWidth = parent.getPreferredSize().width - parentInsets.left - parentInsets.right;
int diff = component.getWidth() - parentWidth;
if (diff > 0)
{
int leftDiff = (int)(diff * alignment);
int rightDiff = diff - leftDiff;
borderInsets.left += leftDiff;
borderInsets.right += rightDiff;
}
}
}
}

View file

@ -0,0 +1,35 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import com.intellij.icons.AllIcons;
import javax.swing.JButton;
import javax.swing.SwingConstants;
public class GenerateButton extends JButton {
public GenerateButton() {
initialize();
}
public void setMode(Mode mode, Runnable onClick) {
var isStopMode = mode == Mode.STOP;
setIcon(isStopMode ? AllIcons.Actions.Suspend : AllIcons.Actions.Refresh);
setText(isStopMode ? "Stop generating" : "Regenerate response");
for (var listener : getActionListeners()) {
removeActionListener(listener);
}
addActionListener(e -> onClick.run());
}
private void initialize() {
setVerticalTextPosition(SwingConstants.CENTER);
setHorizontalTextPosition(SwingConstants.RIGHT);
setVerticalAlignment(SwingConstants.CENTER);
setAlignmentY(0.5f);
setVisible(false);
}
public enum Mode {
STOP,
REFRESH
}
}

View file

@ -0,0 +1,42 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import static java.util.stream.Collectors.toList;
import icons.Icons;
import java.awt.GridBagLayout;
import java.util.List;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
public class LandingView {
private static final List<String> questions = List.of("How do I make an HTTP request in Javascript?",
"What is the difference between px, dip, dp, and sp?",
"How do I undo the most recent local commits in Git?",
"What is the difference between stack and heap?");
public List<JPanel> getQuestionPanels() {
return questions.stream()
.map(this::createQuestionPanel)
.collect(toList());
}
public JPanel createImageIconPanel() {
var imageIconPanel = new JPanel();
imageIconPanel.setLayout(new GridBagLayout());
var imageIconLabel = new JLabel(Icons.SunImageIcon);
imageIconLabel.setHorizontalAlignment(JLabel.CENTER);
imageIconPanel.add(imageIconLabel);
return imageIconPanel;
}
private JPanel createQuestionPanel(String question) {
var panel = new JPanel();
panel.setLayout(new GridBagLayout());
var label = new JLabel(question, SwingConstants.CENTER);
label.setHorizontalAlignment(JLabel.CENTER);
panel.add(label);
return panel;
}
}

View file

@ -0,0 +1,31 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.ui.components.JBScrollPane;
import com.intellij.util.ui.JBUI;
import java.awt.Adjustable;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import javax.swing.JScrollBar;
import javax.swing.ScrollPaneConstants;
public class ScrollPane extends JBScrollPane {
public ScrollPane(ScrollablePanel scrollablePanel) {
super(scrollablePanel);
setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
setBorder(JBUI.Borders.empty(0, 8));
setViewportBorder(null);
}
public void scrollToBottom() {
JScrollBar verticalBar = getVerticalScrollBar();
verticalBar.addAdjustmentListener(new AdjustmentListener() {
public void adjustmentValueChanged(AdjustmentEvent e) {
Adjustable adjustable = e.getAdjustable();
adjustable.setValue(adjustable.getMaximum());
verticalBar.removeAdjustmentListener(this);
}
});
}
}

View file

@ -0,0 +1,68 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import com.intellij.ui.JBColor;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import javax.swing.AbstractAction;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
public class SwingUtils {
public static JTextArea createTextArea(String selectedText) {
var textArea = new JTextArea();
textArea.append(selectedText);
textArea.setLineWrap(true);
textArea.setEditable(false);
textArea.setFont(textArea.getFont().deriveFont(Font.ITALIC));
textArea.setWrapStyleWord(true);
textArea.setBackground(JBColor.PanelBackground);
return textArea;
}
public static JLabel createIconLabel(Icon icon, String text) {
var iconLabel = new JLabel(icon);
iconLabel.setText(text);
iconLabel.setFont(iconLabel.getFont().deriveFont(iconLabel.getFont().getStyle() | Font.BOLD));
iconLabel.setIconTextGap(8);
return iconLabel;
}
public static JButton createIconButton(Icon icon) {
var button = new JButton(icon);
button.setBorder(BorderFactory.createEmptyBorder());
button.setContentAreaFilled(false);
button.setPreferredSize(new Dimension(icon.getIconWidth(), icon.getIconHeight()));
return button;
}
public static Box justifyLeft(Component component) {
Box box = Box.createHorizontalBox();
box.add(component);
box.add(Box.createHorizontalGlue());
return box;
}
public static void addShiftEnterInputMap(JTextArea textArea, Runnable onEnter) {
var input = textArea.getInputMap();
var enterStroke = KeyStroke.getKeyStroke("ENTER");
var shiftEnterStroke = KeyStroke.getKeyStroke("shift ENTER");
input.put(shiftEnterStroke, "insert-break");
input.put(enterStroke, "text-submit");
var actions = textArea.getActionMap();
actions.put("text-submit", new AbstractAction() {
public void actionPerformed(ActionEvent e) {
onEnter.run();
}
});
}
}

View file

@ -0,0 +1,75 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.createIconButton;
import com.intellij.icons.AllIcons;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.StringSelection;
import java.io.IOException;
import javax.swing.JButton;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.Theme;
import org.fife.ui.rtextarea.CaretStyle;
public class SyntaxTextArea extends RSyntaxTextArea {
public SyntaxTextArea(boolean isReadOnly, boolean withBlockCaret, String syntax) {
super("");
setStyles(isReadOnly, withBlockCaret, syntax);
}
public void displayCopyButton() {
ComponentBorder cb = new ComponentBorder(createCopyButton());
cb.setAlignment(TOP_ALIGNMENT);
cb.install(this);
}
public void hideCaret() {
getCaret().setVisible(false);
}
public void changeStyleViaThemeXml() {
var baseThemePath = "/org/fife/ui/rsyntaxtextarea/themes/";
try {
Theme theme = Theme.load(getClass().getResourceAsStream(
UIUtil.isUnderDarcula() ? baseThemePath + "dark.xml" : baseThemePath + "idea.xml"));
theme.apply(this);
} catch (IOException e) {
e.printStackTrace();
}
}
private void setStyles(boolean isReadOnly, boolean withBlockCaret, String syntax) {
setMargin(JBUI.insets(5));
setAntiAliasingEnabled(true);
setEnabled(true);
setEditable(!isReadOnly);
setPaintTabLines(false);
setHighlightCurrentLine(false);
setLineWrap(true);
if (withBlockCaret) {
setCaretStyle(0, CaretStyle.BLOCK_STYLE);
getCaret().setVisible(true);
}
setSyntaxEditingStyle(syntax);
changeStyleViaThemeXml();
}
private void copyToClipboard() {
StringSelection stringSelection = new StringSelection(getText().trim());
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(stringSelection, null);
}
private JButton createCopyButton() {
var button = createIconButton(AllIcons.General.InlineCopy);
button.addActionListener(e -> {
copyToClipboard();
button.setIcon(AllIcons.General.InspectionsOK);
});
return button;
}
}

View file

@ -0,0 +1,57 @@
package ee.carlrobert.codegpt.ide.toolwindow.components;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.addShiftEnterInputMap;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.createIconButton;
import com.intellij.ui.JBColor;
import com.intellij.ui.components.JBTextArea;
import com.intellij.util.ui.JBUI;
import icons.Icons;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import javax.swing.JButton;
import javax.swing.JScrollPane;
public class TextArea extends JBTextArea {
public TextArea(Runnable onSubmit, JScrollPane textAreaScrollPane) {
super("Ask me anything...");
setForeground(JBColor.GRAY);
setMargin(JBUI.insets(5));
addFocusListener(getFocusListener());
addSubmitButton(onSubmit, textAreaScrollPane);
addShiftEnterInputMap(this, onSubmit);
}
private void addSubmitButton(Runnable onSubmit, JScrollPane textAreaScrollPane) {
var button = createSubmitButton(e -> onSubmit.run());
ComponentBorder cb = new ComponentBorder(button);
cb.setAdjustInsets(true);
cb.install(textAreaScrollPane);
}
private JButton createSubmitButton(ActionListener submitButtonListener) {
var button = createIconButton(Icons.SendImageIcon);
button.addActionListener(submitButtonListener);
return button;
}
private FocusListener getFocusListener() {
return new FocusListener() {
public void focusGained(FocusEvent e) {
if (getText().equals("Ask me anything...")) {
setText("");
setForeground(JBColor.BLACK);
}
}
public void focusLost(FocusEvent e) {
if (getText().isEmpty()) {
setForeground(JBColor.GRAY);
setText("Ask me anything...");
}
}
};
}
}

View file

@ -0,0 +1,93 @@
package ee.carlrobert.codegpt.ide.toolwindow.conversations;
import static ee.carlrobert.codegpt.ide.toolwindow.components.SwingUtils.justifyLeft;
import com.intellij.icons.AllIcons;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.JBUI;
import ee.carlrobert.codegpt.ide.conversations.Conversation;
import java.awt.Cursor;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.time.format.DateTimeFormatter;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JPanel;
class ConversationPanel extends JPanel {
private final Conversation conversation;
ConversationPanel(Conversation conversation, boolean isSelected) {
this.conversation = conversation;
addStyles(isSelected);
var constraints = new GridBagConstraints();
constraints.insets = JBUI.insets(0, 10);
addChatIcon(constraints);
addTextPanel(constraints);
}
private void addStyles(boolean isSelected) {
setBackground(JBColor.background().darker());
if (isSelected) {
setBorder(BorderFactory.createCompoundBorder(
BorderFactory.createMatteBorder(4, 4, 4, 4, JBColor.green),
JBUI.Borders.empty(10)));
} else {
setBorder(JBUI.Borders.empty(10));
}
setLayout(new GridBagLayout());
setCursor(new Cursor(Cursor.HAND_CURSOR));
}
private void addChatIcon(GridBagConstraints constraints) {
constraints.gridx = 0;
constraints.gridy = 0;
constraints.weightx = 0.0;
constraints.fill = GridBagConstraints.NONE;
add(new JLabel(AllIcons.Actions.Annotate), constraints);
}
private void addTextPanel(GridBagConstraints constraints) {
constraints.gridx = 1;
constraints.weightx = 1.0;
constraints.fill = GridBagConstraints.HORIZONTAL;
add(createTextPanel(), constraints);
}
private JPanel createTextPanel() {
var title = new JLabel(getFirstPrompt());
title.setBorder(JBUI.Borders.emptyBottom(8));
title.setFont(title.getFont().deriveFont(title.getFont().getStyle() | Font.BOLD));
var textPanel = new JPanel();
textPanel.setBackground(getBackground());
textPanel.setLayout(new BoxLayout(textPanel, BoxLayout.PAGE_AXIS));
textPanel.add(justifyLeft(title));
var bottomPanel = new JPanel();
bottomPanel.setBackground(getBackground());
bottomPanel.setLayout(new BoxLayout(bottomPanel, BoxLayout.X_AXIS));
bottomPanel.add(new JLabel(conversation.getUpdatedOn()
.format(DateTimeFormatter.ofPattern("M/d/yyyy, h:mm:ss a"))));
bottomPanel.add(Box.createHorizontalGlue());
if (conversation.getModel() != null) {
bottomPanel.add(new JLabel(conversation.getModel().getCode()));
}
textPanel.add(bottomPanel);
return textPanel;
}
private String getFirstPrompt() {
var messages = conversation.getMessages();
var prompt = "";
if (!messages.isEmpty()) {
prompt = conversation.getMessages().get(0).getPrompt();
}
return prompt;
}
}

View file

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.codegpt.ide.toolwindow.conversations.ConversationsToolWindow">
<grid id="27dc6" binding="conversationsToolWindowContent" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<scrollpane id="77046" binding="scrollPane" custom-create="true">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</scrollpane>
</children>
</grid>
</form>

View file

@ -0,0 +1,69 @@
package ee.carlrobert.codegpt.ide.toolwindow.conversations;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.codegpt.ide.conversations.Conversation;
import ee.carlrobert.codegpt.ide.conversations.ConversationsState;
import ee.carlrobert.codegpt.ide.toolwindow.ContentManagerService;
import ee.carlrobert.codegpt.ide.toolwindow.ToolWindowService;
import java.util.Comparator;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import org.jetbrains.annotations.NotNull;
public class ConversationsToolWindow {
private final Project project;
private JPanel conversationsToolWindowContent;
private JScrollPane scrollPane;
private ScrollablePanel scrollablePanel;
public ConversationsToolWindow(@NotNull Project project) {
this.project = project;
refresh();
}
public JPanel getContent() {
return conversationsToolWindowContent;
}
public void refresh() {
scrollablePanel.removeAll();
ConversationsState.getInstance()
.conversationsContainer
.getConversationsMapping()
.forEach((key, value) -> value.stream()
.sorted(Comparator.comparing(Conversation::getUpdatedOn).reversed())
.forEach(this::addContent));
}
private void addContent(Conversation conversation) {
var mainPanel = new RootConversationPanel(() -> {
ConversationsState.getInstance().setCurrentConversation(conversation);
ContentManagerService.getInstance(project).displayChatTab();
project.getService(ToolWindowService.class)
.getChatToolWindow()
.displayConversation(conversation);
});
var currentConversation = ConversationsState.getCurrentConversation();
var isSelected = currentConversation != null && currentConversation.getId().equals(conversation.getId());
mainPanel.setBackground(conversationsToolWindowContent.getBackground());
mainPanel.add(new ConversationPanel(conversation, isSelected));
scrollablePanel.add(mainPanel);
}
private void createUIComponents() {
scrollablePanel = new ScrollablePanel();
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
scrollPane = new JBScrollPane();
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setViewportView(scrollablePanel);
scrollPane.setBorder(null);
scrollPane.setViewportBorder(null);
}
}

View file

@ -0,0 +1,43 @@
package ee.carlrobert.codegpt.ide.toolwindow.conversations;
import com.intellij.ui.JBColor;
import com.intellij.util.ui.JBUI;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
class RootConversationPanel extends JPanel {
RootConversationPanel(Runnable onClick) {
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
setBorder(JBUI.Borders.empty(10, 20));
setBackground(JBColor.background());
addMouseListener(getMouseListener(onClick));
}
private MouseListener getMouseListener(Runnable onClick) {
return new MouseListener() {
@Override
public void mouseClicked(MouseEvent mouseEvent) {
onClick.run();
}
@Override
public void mousePressed(MouseEvent mouseEvent) {
}
@Override
public void mouseReleased(MouseEvent mouseEvent) {
}
@Override
public void mouseEntered(MouseEvent mouseEvent) {
}
@Override
public void mouseExited(MouseEvent mouseEvent) {
}
};
}
}