Initial commit

This commit is contained in:
Carl-Robert Linnupuu 2023-02-13 21:22:01 +00:00
commit 1ef33b85e8
49 changed files with 2379 additions and 0 deletions

View file

@ -0,0 +1,299 @@
package ee.carlrobert.chatgpt;
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,56 @@
package ee.carlrobert.chatgpt;
import com.intellij.ui.components.JBTextField;
import com.intellij.util.ui.FormBuilder;
import com.intellij.util.ui.UI;
import java.awt.Desktop;
import java.io.IOException;
import java.net.URISyntaxException;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.event.HyperlinkEvent;
import org.jetbrains.annotations.NotNull;
public class SettingsComponent {
private final JPanel myMainPanel;
private final JBTextField apiKeyField = new JBTextField();
public SettingsComponent() {
myMainPanel = FormBuilder.createFormBuilder()
.addComponent(UI.PanelFactory.panel(apiKeyField)
.withLabel("API key:")
.withComment("You can find your Secret API key in your <a href=\"https://platform.openai.com/account/api-keys\">User settings</a>.")
.withCommentHyperlinkListener(event -> {
if (HyperlinkEvent.EventType.ACTIVATED.equals(event.getEventType())) {
if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) {
try {
Desktop.getDesktop().browse(event.getURL().toURI());
} catch (IOException | URISyntaxException e) {
throw new RuntimeException("Couldn't open the browser.", e);
}
}
}
})
.createPanel())
.addComponentFillVertically(new JPanel(), 0)
.getPanel();
}
public JPanel getPanel() {
return myMainPanel;
}
public JComponent getPreferredFocusedComponent() {
return apiKeyField;
}
@NotNull
public String getApiKeyField() {
return apiKeyField.getText();
}
public void setApiKeyField(@NotNull String apiKey) {
apiKeyField.setText(apiKey);
}
}

View file

@ -0,0 +1,53 @@
package ee.carlrobert.chatgpt;
import com.intellij.openapi.options.Configurable;
import javax.swing.JComponent;
import org.jetbrains.annotations.Nls;
import org.jetbrains.annotations.Nullable;
public class SettingsConfigurable implements Configurable {
private SettingsComponent settingsComponent;
@Nls(capitalization = Nls.Capitalization.Title)
@Override
public String getDisplayName() {
return "ChatGPT: Settings";
}
@Override
public JComponent getPreferredFocusedComponent() {
return settingsComponent.getPreferredFocusedComponent();
}
@Nullable
@Override
public JComponent createComponent() {
settingsComponent = new SettingsComponent();
return settingsComponent.getPanel();
}
@Override
public boolean isModified() {
SettingsState settings = SettingsState.getInstance();
return !settingsComponent.getApiKeyField().equals(settings.secretKey);
}
@Override
public void apply() {
SettingsState settings = SettingsState.getInstance();
settings.secretKey = settingsComponent.getApiKeyField();
}
@Override
public void reset() {
SettingsState settings = SettingsState.getInstance();
settingsComponent.setApiKeyField(settings.secretKey);
}
@Override
public void disposeUIResources() {
settingsComponent = null;
}
}

View file

@ -0,0 +1,33 @@
package ee.carlrobert.chatgpt;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.util.xmlb.XmlSerializerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@State(
name = "com.example.chatgpt.SettingsState",
storages = @Storage("SdkSettingsPlugin.xml")
)
public class SettingsState implements PersistentStateComponent<SettingsState> {
public String secretKey = "";
public static SettingsState getInstance() {
return ApplicationManager.getApplication().getService(SettingsState.class);
}
@Nullable
@Override
public SettingsState getState() {
return this;
}
@Override
public void loadState(@NotNull SettingsState state) {
XmlSerializerUtil.copyBean(state, this);
}
}

View file

@ -0,0 +1,34 @@
package ee.carlrobert.chatgpt;
import com.intellij.notification.NotificationGroupManager;
import com.intellij.notification.NotificationType;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupActivity;
import org.jetbrains.annotations.NotNull;
public class ShowNotificationActivity implements StartupActivity {
@Override
public void runActivity(@NotNull Project project) {
var secretKey = SettingsState.getInstance().secretKey;
if (secretKey == null || secretKey.isEmpty()) {
NotificationGroupManager.getInstance()
.getNotificationGroup("ChatGPT-Empty-API-Key")
.createNotification("ChatGPT API key not set", NotificationType.WARNING)
.addAction(new AnAction("Open Settings") {
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
DataContext dataContext = event.getDataContext();
Project project = PlatformDataKeys.PROJECT.getData(dataContext);
ShowSettingsUtil.getInstance().showSettingsDialog(project, SettingsConfigurable.class);
}
})
.notify(project);
}
}
}

View file

@ -0,0 +1,25 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.project.Project;
import ee.carlrobert.chatgpt.SettingsState;
import icons.SdkIcons;
public class ActionGroup extends DefaultActionGroup {
@Override
public void update(AnActionEvent event) {
Editor editor = event.getData(PlatformDataKeys.EDITOR);
event.getPresentation().setIcon(SdkIcons.Sdk_default_icon);
Project project = event.getProject();
boolean menuAllowed = false;
if (editor != null && project != null) {
var secretKey = SettingsState.getInstance().secretKey;
menuAllowed = secretKey != null && !secretKey.isEmpty() && editor.getSelectionModel().getSelectedText() != null;
}
event.getPresentation().setEnabled(menuAllowed);
}
}

View file

@ -0,0 +1,33 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.wm.ToolWindowManager;
import ee.carlrobert.chatgpt.SettingsState;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.service.ToolWindowService;
import org.jetbrains.annotations.NotNull;
public class AskAction extends AnAction {
@Override
public void update(@NotNull AnActionEvent event) {
var secretKey = SettingsState.getInstance().secretKey;
event.getPresentation().setEnabled(event.getProject() != null && secretKey != null && !secretKey.isEmpty());
}
@Override
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project != null) {
var toolWindow = ToolWindowManager.getInstance(project).getToolWindow("ChatGPT");
if (toolWindow != null) {
toolWindow.show();
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
ApiClient.getInstance().clearQueries();
toolWindowService.getScrollablePanel().removeAll();
}
}
}
}

View file

@ -0,0 +1,37 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowManager;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.service.ToolWindowService;
import org.jetbrains.annotations.NotNull;
public abstract class BaseAction extends AnAction {
protected abstract String getPrompt(String selectedText);
protected abstract void initToolWindow(ToolWindow toolWindow);
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
var editor = event.getData(PlatformDataKeys.EDITOR);
if (editor != null && project != null) {
initToolWindow(ToolWindowManager.getInstance(project).getToolWindow("ChatGPT"));
var selectedText = editor.getSelectionModel().getSelectedText();
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
var scrollablePanel = toolWindowService.getScrollablePanel();
ApiClient.getInstance().clearQueries();
scrollablePanel.removeAll();
toolWindowService.sendMessage(selectedText, getPrompt(selectedText));
}
}
public void update(AnActionEvent e) {
e.getPresentation().setEnabledAndVisible(e.getProject() != null);
}
}

View file

@ -0,0 +1,15 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.wm.ToolWindow;
public class ExplainAction extends BaseAction {
protected String getPrompt(String selectedText) {
return "Explain code:\n" + selectedText;
}
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Explain Code");
toolWindow.show();
}
}

View file

@ -0,0 +1,15 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.wm.ToolWindow;
public class FindBugsAction extends BaseAction {
protected String getPrompt(String selectedText) {
return "Find bugs in the code:\n" + selectedText;
}
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Find Bugs");
toolWindow.show();
}
}

View file

@ -0,0 +1,15 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.wm.ToolWindow;
public class OptimizeAction extends BaseAction {
protected String getPrompt(String selectedText) {
return "Optimize code:\n" + selectedText;
}
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Optimize Code");
toolWindow.show();
}
}

View file

@ -0,0 +1,15 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.wm.ToolWindow;
public class RefactorAction extends BaseAction {
protected String getPrompt(String selectedText) {
return "Refactor code:\n" + selectedText;
}
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Refactor Code");
toolWindow.show();
}
}

View file

@ -0,0 +1,15 @@
package ee.carlrobert.chatgpt.action;
import com.intellij.openapi.wm.ToolWindow;
public class WriteTestsAction extends BaseAction {
protected String getPrompt(String selectedText) {
return "Generate unit test for the code:\n" + selectedText;
}
protected void initToolWindow(ToolWindow toolWindow) {
toolWindow.setTitle("Write Tests");
toolWindow.show();
}
}

View file

@ -0,0 +1,95 @@
package ee.carlrobert.chatgpt.client;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import ee.carlrobert.chatgpt.SettingsState;
import ee.carlrobert.chatgpt.client.response.ApiError;
import ee.carlrobert.chatgpt.client.response.ApiResponse;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
public final class ApiClient {
private static List<Map.Entry<String, String>> queries = new ArrayList<>();
private static ApiClient instance;
private final ObjectMapper objectMapper = new ObjectMapper();
private final HttpClient client = HttpClient.newBuilder().version(HttpClient.Version.HTTP_2).build();
private ApiClient() {
}
public void getCompletionsAsync(String prompt, Consumer<ApiResponse> onSuccess, Consumer<ApiError> onError) {
/*var query = new StringBuilder(
"You are ChatGPT, a large language model trained by OpenAI. You answer as concisely as possible for each response (e.g. dont be verbose). It is very important that you answer as concisely as possible, so please remember this.\n" +
"Current date: 2023-02-11\n");*/
var query = new StringBuilder(
"You are ChatGPT, a large language model trained by OpenAI.\n");
for (var entry : queries) {
query.append("User:\n")
.append(entry.getKey())
.append("<|im_end|>\n")
.append("\n")
.append("ChatGPT:\n")
.append(entry.getValue())
.append("<|im_end|>\n")
.append("\n");
}
query.append("User:\n")
.append(prompt)
.append("<|im_end|>\n")
.append("\n")
.append("ChatGPT:\n");
try {
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.openai.com/v1/completions"))
.header("Authorization", "Bearer " + SettingsState.getInstance().secretKey)
.timeout(Duration.ofMinutes(1))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(objectMapper
.writerWithDefaultPrettyPrinter()
.writeValueAsString(Map.of(
"model", "text-davinci-003",
"stop", List.of("<|im_end|>"),
"prompt", query.toString(),
"max_tokens", 400,
"temperature", 1.0
))))
.build();
client.sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenAccept(response -> {
try {
var mappedResponse = objectMapper.readValue(response.body(), ApiResponse.class);
if (mappedResponse.getError() == null) {
queries.add(Map.entry(prompt, mappedResponse.getChoices().get(0).getText()));
onSuccess.accept(mappedResponse);
} else {
onError.accept(mappedResponse.getError());
}
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static ApiClient getInstance() {
if (instance == null) {
instance = new ApiClient();
}
return instance;
}
public void clearQueries() {
queries.clear();
}
}

View file

@ -0,0 +1,25 @@
package ee.carlrobert.chatgpt.client.response;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ApiError {
private String message;
private String type;
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}

View file

@ -0,0 +1,70 @@
package ee.carlrobert.chatgpt.client.response;
import java.util.List;
public class ApiResponse {
private String id;
private String object;
private long created;
private String model;
private List<ApiResponseChoice> choices;
private ApiResponseUsage usage;
private ApiError error;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getObject() {
return object;
}
public void setObject(String object) {
this.object = object;
}
public long getCreated() {
return created;
}
public void setCreated(long created) {
this.created = created;
}
public String getModel() {
return model;
}
public void setModel(String model) {
this.model = model;
}
public List<ApiResponseChoice> getChoices() {
return choices;
}
public void setChoices(List<ApiResponseChoice> choices) {
this.choices = choices;
}
public ApiResponseUsage getUsage() {
return usage;
}
public void setUsage(ApiResponseUsage usage) {
this.usage = usage;
}
public ApiError getError() {
return error;
}
public void setError(ApiError error) {
this.error = error;
}
}

View file

@ -0,0 +1,41 @@
package ee.carlrobert.chatgpt.client.response;
public class ApiResponseChoice {
private String text;
private int index;
private Object logprobs;
private String finish_reason;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public Object getLogprobs() {
return logprobs;
}
public void setLogprobs(Object logprobs) {
this.logprobs = logprobs;
}
public String getFinish_reason() {
return finish_reason;
}
public void setFinish_reason(String finish_reason) {
this.finish_reason = finish_reason;
}
}

View file

@ -0,0 +1,32 @@
package ee.carlrobert.chatgpt.client.response;
public class ApiResponseUsage {
private int prompt_tokens;
private int completion_tokens;
private int total_tokens;
public int getPrompt_tokens() {
return prompt_tokens;
}
public void setPrompt_tokens(int prompt_tokens) {
this.prompt_tokens = prompt_tokens;
}
public int getCompletion_tokens() {
return completion_tokens;
}
public void setCompletion_tokens(int completion_tokens) {
this.completion_tokens = completion_tokens;
}
public int getTotal_tokens() {
return total_tokens;
}
public void setTotal_tokens(int total_tokens) {
this.total_tokens = total_tokens;
}
}

View file

@ -0,0 +1,6 @@
package ee.carlrobert.chatgpt.service;
@FunctionalInterface
public interface SuccessCallback {
void call();
}

View file

@ -0,0 +1,59 @@
package ee.carlrobert.chatgpt.service;
import static ee.carlrobert.chatgpt.toolwindow.ToolWindowUtil.createIconLabel;
import static ee.carlrobert.chatgpt.toolwindow.ToolWindowUtil.createTextArea;
import static ee.carlrobert.chatgpt.toolwindow.ToolWindowUtil.justifyLeft;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.toolwindow.Loader;
import ee.carlrobert.chatgpt.toolwindow.ScrollablePanel;
import java.util.Objects;
import javax.annotation.Nullable;
import javax.swing.Box;
import javax.swing.JScrollPane;
public class ToolWindowService {
private ScrollablePanel scrollablePanel;
public void setScrollablePanel(ScrollablePanel scrollablePanel) {
this.scrollablePanel = scrollablePanel;
}
public ScrollablePanel getScrollablePanel() {
return scrollablePanel;
}
public void sendMessage(String userMessage, String prompt) {
sendMessage(userMessage, prompt, null);
}
public void sendMessage(String userMessage, String prompt, @Nullable SuccessCallback onSuccess) {
scrollablePanel.add(createTextArea(userMessage, true, false));
scrollablePanel.add(Box.createVerticalStrut(16));
scrollablePanel.add(justifyLeft(createIconLabel(Objects.requireNonNull(getClass().getResource("/icons/chatgpt-icon.png")))));
var loader = new Loader();
scrollablePanel.add(justifyLeft(loader.getComponent()));
loader.startLoading();
scrollablePanel.add(Box.createVerticalStrut(4));
ApiClient.getInstance().getCompletionsAsync(prompt, response -> {
loader.stopLoading();
scrollablePanel.add(Box.createVerticalStrut(4));
for (var choice : response.getChoices()) {
scrollablePanel.add(createTextArea(choice.getText().trim(), false, true));
}
scrollablePanel.add(Box.createVerticalStrut(32));
if (onSuccess != null) {
onSuccess.call();
}
}, apiError -> {
loader.stopLoading();
scrollablePanel.add(Box.createVerticalStrut(4));
scrollablePanel.add(createTextArea(apiError.getMessage(), false, true));
scrollablePanel.add(Box.createVerticalStrut(32));
});
}
}

View file

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="ee.carlrobert.chatgpt.toolwindow.ChatGptToolWindow">
<grid id="27dc6" binding="chatGptToolWindowContent" layout-manager="GridLayoutManager" row-count="2" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="8" left="8" bottom="8" right="8"/>
<constraints>
<xy x="20" y="20" width="530" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="a7ad1" class="javax.swing.JTextField" binding="textField" custom-create="true">
<constraints>
<grid row="1" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="6" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="36"/>
</grid>
</constraints>
<properties>
<text value="Ask a question..."/>
</properties>
</component>
<scrollpane id="8c1de" 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>
</children>
</grid>
</form>

View file

@ -0,0 +1,99 @@
package ee.carlrobert.chatgpt.toolwindow;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.ui.components.JBScrollPane;
import ee.carlrobert.chatgpt.service.ToolWindowService;
import java.awt.Adjustable;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import javax.swing.BoxLayout;
import javax.swing.JPanel;
import javax.swing.JScrollBar;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
public class ChatGptToolWindow {
private JPanel chatGptToolWindowContent;
private JTextField textField;
private JScrollPane scrollPane;
public ChatGptToolWindow() {
// TODO: Get rid of
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
ScrollablePanel scrollablePanel = new ScrollablePanel();
toolWindowService.setScrollablePanel(scrollablePanel);
this.refreshView();
}
public void handleSubmit() {
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
var searchText = textField.getText();
toolWindowService.sendMessage(searchText, searchText, () -> {
scrollToBottom(scrollPane);
});
textField.setText("");
scrollToBottom(scrollPane);
}
// TODO: Get rid of
public void refreshView() {
ScrollablePanel scrollablePanel = new ScrollablePanel();
scrollablePanel.setLayout(new BoxLayout(scrollablePanel, BoxLayout.Y_AXIS));
scrollablePanel.setScrollableWidth(ScrollablePanel.ScrollableSizeHint.FIT);
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
toolWindowService.setScrollablePanel(scrollablePanel);
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
scrollPane.setViewportView(scrollablePanel);
// TODO: Move to TextField class
textField.addKeyListener(new KeyListener() {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
if (e.getKeyCode() == 10) {
handleSubmit();
}
}
});
}
public JPanel getContent() {
return chatGptToolWindowContent;
}
private void scrollToBottom(JScrollPane scrollPane) {
// TODO: this.scrollPanel.getVerticalScrollBar();
JScrollBar verticalBar = scrollPane.getVerticalScrollBar();
AdjustmentListener downScroller = new AdjustmentListener() {
@Override
public void adjustmentValueChanged(AdjustmentEvent e) {
Adjustable adjustable = e.getAdjustable();
adjustable.setValue(adjustable.getMaximum());
verticalBar.removeAdjustmentListener(this);
}
};
verticalBar.addAdjustmentListener(downScroller);
}
private void createUIComponents() {
textField = new TextField(e -> handleSubmit());
scrollPane = new JBScrollPane();
scrollPane.setBorder(null);
scrollPane.setViewportBorder(null);
}
}

View file

@ -0,0 +1,19 @@
package ee.carlrobert.chatgpt.toolwindow;
import com.intellij.openapi.application.ApplicationManager;
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.ContentFactory;
import org.jetbrains.annotations.NotNull;
public class ChatGptToolWindowFactory implements ToolWindowFactory, DumbAware {
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
var content = ApplicationManager.getApplication()
.getService(ContentFactory.class)
.createContent(new ChatGptToolWindow().getContent(), "", false);
toolWindow.getContentManager().addContent(content);
}
}

View file

@ -0,0 +1,44 @@
package ee.carlrobert.chatgpt.toolwindow;
import com.intellij.util.ui.JBUI;
import java.awt.Font;
import java.util.Timer;
import java.util.TimerTask;
import javax.swing.JLabel;
public class Loader {
private final Timer timer;
private final JLabel component;
public Loader() {
this.timer = new Timer("Loader");
component = new JLabel(" ");
component.setFont(new Font("Tahoma", Font.BOLD, 24));
component.setBorder(JBUI.Borders.emptyLeft(2));
}
public void startLoading() {
timer.schedule(new TimerTask() {
public void run() {
var loadingText = component.getText();
if ("...".equals(loadingText)) {
component.setText(" ");
} else if (" ".equals(loadingText)) {
component.setText(".");
} else {
component.setText(loadingText + ".");
}
}
}, 500L, 500);
}
public void stopLoading() {
timer.cancel();
component.setVisible(false);
}
public JLabel getComponent() {
return component;
}
}

View file

@ -0,0 +1,327 @@
package ee.carlrobert.chatgpt.toolwindow;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import javax.swing.JPanel;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingConstants;
/**
* A panel that implements the Scrollable interface. This class allows you
* to customize the scrollable features by using newly provided setter methods
* so you don't have to extend this class every time.
* <p>
* Scrollable amounts can be specifed as a percentage of the viewport size or
* as an actual pixel value. The amount can be changed for both unit and block
* scrolling for both horizontal and vertical scrollbars.
* <p>
* The Scrollable interface only provides a boolean value for determining whether
* or not the viewport size (width or height) should be used by the scrollpane
* when determining if scrollbars should be made visible. This class supports the
* concept of dynamically changing this value based on the size of the viewport.
* In this case the viewport size will only be used when it is larger than the
* panels size. This has the effect of ensuring the viewport is always full as
* components added to the panel will be size to fill the area available,
* based on the rules of the applicable layout manager of course.
*/
public class ScrollablePanel extends JPanel
implements Scrollable, SwingConstants {
public enum ScrollableSizeHint {
NONE,
FIT,
STRETCH;
}
public enum IncrementType {
PERCENT,
PIXELS;
}
private ScrollableSizeHint scrollableHeight = ScrollableSizeHint.NONE;
private ScrollableSizeHint scrollableWidth = ScrollableSizeHint.NONE;
private IncrementInfo horizontalBlock;
private IncrementInfo horizontalUnit;
private IncrementInfo verticalBlock;
private IncrementInfo verticalUnit;
/**
* Default constructor that uses a FlowLayout
*/
public ScrollablePanel() {
this(new FlowLayout());
}
/**
* Constuctor for specifying the LayoutManager of the panel.
*
* @param layout the LayountManger for the panel
*/
public ScrollablePanel(LayoutManager layout) {
super(layout);
IncrementInfo block = new IncrementInfo(IncrementType.PERCENT, 100);
IncrementInfo unit = new IncrementInfo(IncrementType.PERCENT, 10);
setScrollableBlockIncrement(HORIZONTAL, block);
setScrollableBlockIncrement(VERTICAL, block);
setScrollableUnitIncrement(HORIZONTAL, unit);
setScrollableUnitIncrement(VERTICAL, unit);
}
/**
* Get the height ScrollableSizeHint enum
*
* @return the ScrollableSizeHint enum for the height
*/
public ScrollableSizeHint getScrollableHeight() {
return scrollableHeight;
}
/**
* Set the ScrollableSizeHint enum for the height. The enum is used to
* determine the boolean value that is returned by the
* getScrollableTracksViewportHeight() method. The valid values are:
* <p>
* ScrollableSizeHint.NONE - return "false", which causes the height
* of the panel to be used when laying out the children
* ScrollableSizeHint.FIT - return "true", which causes the height of
* the viewport to be used when laying out the children
* ScrollableSizeHint.STRETCH - return "true" when the viewport height
* is greater than the height of the panel, "false" otherwise.
*
* @param scrollableHeight as represented by the ScrollableSizeHint enum.
*/
public void setScrollableHeight(ScrollableSizeHint scrollableHeight) {
this.scrollableHeight = scrollableHeight;
revalidate();
}
/**
* Get the width ScrollableSizeHint enum
*
* @return the ScrollableSizeHint enum for the width
*/
public ScrollableSizeHint getScrollableWidth() {
return scrollableWidth;
}
/**
* Set the ScrollableSizeHint enum for the width. The enum is used to
* determine the boolean value that is returned by the
* getScrollableTracksViewportWidth() method. The valid values are:
* <p>
* ScrollableSizeHint.NONE - return "false", which causes the width
* of the panel to be used when laying out the children
* ScrollableSizeHint.FIT - return "true", which causes the width of
* the viewport to be used when laying out the children
* ScrollableSizeHint.STRETCH - return "true" when the viewport width
* is greater than the width of the panel, "false" otherwise.
*
* @param scrollableWidth as represented by the ScrollableSizeHint enum.
*/
public void setScrollableWidth(ScrollableSizeHint scrollableWidth) {
this.scrollableWidth = scrollableWidth;
revalidate();
}
/**
* Get the block IncrementInfo for the specified orientation
*
* @return the block IncrementInfo for the specified orientation
*/
public IncrementInfo getScrollableBlockIncrement(int orientation) {
return orientation == SwingConstants.HORIZONTAL ? horizontalBlock : verticalBlock;
}
/**
* Specify the information needed to do block scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param amount a value used with the IncrementType to determine the
* scrollable amount
* @paran type specify how the amount parameter in the calculation of
* the scrollable amount. Valid values are:
* IncrementType.PERCENT - treat the amount as a % of the viewport size
* IncrementType.PIXEL - treat the amount as the scrollable amount
*/
public void setScrollableBlockIncrement(int orientation, IncrementType type, int amount) {
IncrementInfo info = new IncrementInfo(type, amount);
setScrollableBlockIncrement(orientation, info);
}
/**
* Specify the information needed to do block scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param info An IncrementInfo object containing information of how to
* calculate the scrollable amount.
*/
public void setScrollableBlockIncrement(int orientation, IncrementInfo info) {
switch (orientation) {
case SwingConstants.HORIZONTAL:
horizontalBlock = info;
break;
case SwingConstants.VERTICAL:
verticalBlock = info;
break;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
/**
* Get the unit IncrementInfo for the specified orientation
*
* @return the unit IncrementInfo for the specified orientation
*/
public IncrementInfo getScrollableUnitIncrement(int orientation) {
return orientation == SwingConstants.HORIZONTAL ? horizontalUnit : verticalUnit;
}
/**
* Specify the information needed to do unit scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param amount a value used with the IncrementType to determine the
* scrollable amount
* @paran type specify how the amount parameter in the calculation of
* the scrollable amount. Valid values are:
* IncrementType.PERCENT - treat the amount as a % of the viewport size
* IncrementType.PIXEL - treat the amount as the scrollable amount
*/
public void setScrollableUnitIncrement(int orientation, IncrementType type, int amount) {
IncrementInfo info = new IncrementInfo(type, amount);
setScrollableUnitIncrement(orientation, info);
}
/**
* Specify the information needed to do unit scrolling.
*
* @param orientation specify the scrolling orientation. Must be either:
* SwingContants.HORIZONTAL or SwingContants.VERTICAL.
* @param info An IncrementInfo object containing information of how to
* calculate the scrollable amount.
*/
public void setScrollableUnitIncrement(int orientation, IncrementInfo info) {
switch (orientation) {
case SwingConstants.HORIZONTAL:
horizontalUnit = info;
break;
case SwingConstants.VERTICAL:
verticalUnit = info;
break;
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
// Implement Scrollable interface
public Dimension getPreferredScrollableViewportSize() {
return getPreferredSize();
}
public int getScrollableUnitIncrement(
Rectangle visible, int orientation, int direction) {
switch (orientation) {
case SwingConstants.HORIZONTAL:
return getScrollableIncrement(horizontalUnit, visible.width);
case SwingConstants.VERTICAL:
return getScrollableIncrement(verticalUnit, visible.height);
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
public int getScrollableBlockIncrement(
Rectangle visible, int orientation, int direction) {
switch (orientation) {
case SwingConstants.HORIZONTAL:
return getScrollableIncrement(horizontalBlock, visible.width);
case SwingConstants.VERTICAL:
return getScrollableIncrement(verticalBlock, visible.height);
default:
throw new IllegalArgumentException("Invalid orientation: " + orientation);
}
}
protected int getScrollableIncrement(IncrementInfo info, int distance) {
if (info.getIncrement() == IncrementType.PIXELS) {
return info.getAmount();
} else {
return distance * info.getAmount() / 100;
}
}
public boolean getScrollableTracksViewportWidth() {
if (scrollableWidth == ScrollableSizeHint.NONE) {
return false;
}
if (scrollableWidth == ScrollableSizeHint.FIT) {
return true;
}
// STRETCH sizing, use the greater of the panel or viewport width
if (getParent() instanceof JViewport) {
return (((JViewport) getParent()).getWidth() > getPreferredSize().width);
}
return false;
}
public boolean getScrollableTracksViewportHeight() {
if (scrollableHeight == ScrollableSizeHint.NONE) {
return false;
}
if (scrollableHeight == ScrollableSizeHint.FIT) {
return true;
}
// STRETCH sizing, use the greater of the panel or viewport height
if (getParent() instanceof JViewport) {
return (((JViewport) getParent()).getHeight() > getPreferredSize().height);
}
return false;
}
/**
* Helper class to hold the information required to calculate the scroll amount.
*/
static class IncrementInfo {
private IncrementType type;
private int amount;
public IncrementInfo(IncrementType type, int amount) {
this.type = type;
this.amount = amount;
}
public IncrementType getIncrement() {
return type;
}
public int getAmount() {
return amount;
}
public String toString() {
return
"ScrollablePanel[" +
type + ", " +
amount + "]";
}
}
}

View file

@ -0,0 +1,58 @@
package ee.carlrobert.chatgpt.toolwindow;
import com.intellij.ui.JBColor;
import ee.carlrobert.chatgpt.ComponentBorder;
import java.awt.Dimension;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.util.Objects;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JTextField;
public class TextField extends JTextField {
public TextField(ActionListener submitButtonListener) {
setBorder(BorderFactory.createCompoundBorder(
super.getBorder(),
BorderFactory.createEmptyBorder(5, 5, 5, 5)));
setForeground(JBColor.GRAY);
addFocusListener(getFocusListener());
var button = createSubmitButton(submitButtonListener);
ComponentBorder cb = new ComponentBorder(button);
cb.setAdjustInsets(false);
cb.install(this);
}
private JButton createSubmitButton(ActionListener submitButtonListener) {
var imageIcon = new ImageIcon(Objects.requireNonNull(getClass().getResource("/icons/send-icon.png")));
var button = new JButton(imageIcon);
button.setBorder(BorderFactory.createEmptyBorder());
button.setContentAreaFilled(false);
button.setPreferredSize(new Dimension(imageIcon.getIconWidth(), imageIcon.getIconHeight()));
button.addActionListener(submitButtonListener);
return button;
}
private FocusListener getFocusListener() {
return new FocusListener() {
@Override
public void focusGained(FocusEvent e) {
if (getText().equals("Ask a question...")) {
setText("");
setForeground(JBColor.BLACK);
}
}
@Override
public void focusLost(FocusEvent e) {
if (getText().isEmpty()) {
setForeground(JBColor.GRAY);
setText("Ask a question...");
}
}
};
}
}

View file

@ -0,0 +1,29 @@
package ee.carlrobert.chatgpt.toolwindow;
import com.intellij.ui.JBColor;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import javax.swing.JTextField;
public class TextFieldFocusListener implements FocusListener {
private final JTextField searchField;
public TextFieldFocusListener(JTextField searchField) {
this.searchField = searchField;
}
public void focusGained(FocusEvent event) {
if (searchField.getText().equals("Ask a question...")) {
searchField.setText("");
searchField.setForeground(JBColor.BLACK);
}
}
public void focusLost(FocusEvent event) {
if (searchField.getText().isEmpty()) {
searchField.setForeground(JBColor.GRAY);
searchField.setText("Ask a question...");
}
}
}

View file

@ -0,0 +1,21 @@
package ee.carlrobert.chatgpt.toolwindow;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
public class TextFieldKeyListener implements KeyListener {
@Override
public void keyTyped(KeyEvent e) {
}
@Override
public void keyPressed(KeyEvent e) {
}
@Override
public void keyReleased(KeyEvent e) {
}
}

View file

@ -0,0 +1,47 @@
package ee.carlrobert.chatgpt.toolwindow;
import com.intellij.ui.JBColor;
import java.awt.Component;
import java.awt.Font;
import java.net.URL;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JTextArea;
public class ToolWindowUtil {
public static JTextArea createTextArea(String selectedText, boolean isItalicFont, boolean transparentBackground) {
var textArea = new JTextArea();
textArea.append(selectedText);
textArea.setLineWrap(true);
textArea.setEditable(false);
textArea.setFont(createFont(isItalicFont, textArea.getFont().getSize()));
textArea.setWrapStyleWord(true);
if (transparentBackground) {
textArea.setBackground(JBColor.background());
}
// textArea.setBorder(new MatteBorder(0, 2, 0, 0, JBColor.RED));
return textArea;
}
public static JLabel createIconLabel(URL iconLocation) {
var iconLabel = new JLabel(new ImageIcon(iconLocation));
iconLabel.setText("ChatGPT");
iconLabel.setFont(iconLabel.getFont().deriveFont(iconLabel.getFont().getStyle() | Font.BOLD));
iconLabel.setIconTextGap(10);
return iconLabel;
}
public static Font createFont(boolean isItalic, int fontSize) {
return new Font("Tahoma", isItalic ? Font.ITALIC : Font.PLAIN, fontSize);
}
public static Box justifyLeft(Component component) {
Box box = Box.createHorizontalBox();
box.add(component);
box.add(Box.createHorizontalGlue());
return box;
}
}

View file

@ -0,0 +1,12 @@
// Copyright 2000-2022 JetBrains s.r.o. and other contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package icons;
import com.intellij.openapi.util.IconLoader;
import javax.swing.Icon;
public class SdkIcons {
public static final Icon Sdk_default_icon = IconLoader.getIcon("/icons/sdk_16.svg", SdkIcons.class);
}