diff --git a/build.gradle.kts b/build.gradle.kts
index f1f50abb..4fdb2ad5 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -4,7 +4,7 @@ plugins {
}
group = "ee.carlrobert"
-version = "1.0.8"
+version = "1.0.9"
repositories {
mavenCentral()
@@ -16,6 +16,10 @@ intellij {
plugins.set(listOf())
}
+dependencies {
+ implementation("com.fifesoft:rsyntaxtextarea:3.3.2")
+}
+
java {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
diff --git a/src/main/java/ee/carlrobert/chatgpt/client/ApiClient.java b/src/main/java/ee/carlrobert/chatgpt/client/ApiClient.java
index ac3a09cb..b5f1d878 100644
--- a/src/main/java/ee/carlrobert/chatgpt/client/ApiClient.java
+++ b/src/main/java/ee/carlrobert/chatgpt/client/ApiClient.java
@@ -12,6 +12,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;
+import javax.annotation.Nullable;
public final class ApiClient {
@@ -30,9 +31,9 @@ public final class ApiClient {
return instance;
}
- public void getCompletionsAsync(String userPrompt, Consumer onMessage) {
+ public void getCompletionsAsync(String userPrompt, Consumer onMessage, @Nullable Consumer onComplete) {
var prompt = buildCompletePrompt(userPrompt);
- this.client.sendAsync(buildHttpRequest(prompt), respInfo -> subscribe(respInfo, userPrompt, onMessage));
+ this.client.sendAsync(buildHttpRequest(prompt), respInfo -> subscribe(respInfo, userPrompt, onMessage, onComplete));
}
public void clearQueries() {
@@ -40,7 +41,11 @@ public final class ApiClient {
}
private String buildCompletePrompt(String prompt) {
- var basePrompt = new StringBuilder("You are ChatGPT, a large language model trained by OpenAI.\n");
+ var basePrompt = new StringBuilder("""
+ You are ChatGPT, a large language model trained by OpenAI.
+ One of your main goals is code generation but not only.
+ Answer in a markdown language. Markdown code blocks should contain language whenever possible.
+ """);
queries.forEach(query ->
basePrompt.append("User:\n")
.append(query.getKey())
@@ -81,11 +86,20 @@ public final class ApiClient {
}
}
- private BodySubscriber subscribe(ResponseInfo responseInfo, String userPrompt, Consumer onMessage) {
+ private BodySubscriber subscribe(
+ ResponseInfo responseInfo,
+ String userPrompt,
+ Consumer onMessage,
+ @Nullable Consumer onComplete) {
if (responseInfo.statusCode() == 200) {
return new Subscriber((messageData ->
onMessage.accept(messageData.choices().get(0).text())),
- (finalMsg) -> queries.add(Map.entry(userPrompt, finalMsg)));
+ (finalMsg) -> {
+ queries.add(Map.entry(userPrompt, finalMsg));
+ if (onComplete != null) {
+ onComplete.accept(finalMsg);
+ }
+ });
} else if (responseInfo.statusCode() == 401) {
onMessage.accept("Incorrect API key provided.\n" +
"You can find your API key at https://platform.openai.com/account/api-keys.");
diff --git a/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java b/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java
index cd0d218e..85e4f54b 100644
--- a/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java
+++ b/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java
@@ -9,13 +9,27 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
-import java.util.concurrent.Flow;
+import java.util.concurrent.Flow.Subscription;
import java.util.function.Consumer;
import java.util.regex.Pattern;
public class Subscriber implements HttpResponse.BodySubscriber {
- protected static final Pattern dataLinePattern = Pattern.compile("^data: ?(.*)$");
+ private static final Pattern dataLinePattern = Pattern.compile("^data: ?(.*)$");
+ private volatile Subscription subscription;
+ private volatile String deferredText;
+ private final Consumer super ApiResponse> messageDataConsumer;
+ private final CompletableFuture future;
+ private final Consumer onComplete;
+ private final StringBuilder msgBuilder = new StringBuilder();
+
+ public Subscriber(Consumer super ApiResponse> messageDataConsumer, Consumer onComplete) {
+ this.messageDataConsumer = messageDataConsumer;
+ this.future = new CompletableFuture<>();
+ this.subscription = null;
+ this.deferredText = null;
+ this.onComplete = onComplete;
+ }
protected static ApiResponse extractMessageData(String[] messageLines) {
var responseBuilder = new StringBuilder();
@@ -33,23 +47,8 @@ public class Subscriber implements HttpResponse.BodySubscriber {
}
}
- protected final Consumer super ApiResponse> messageDataConsumer;
- protected final CompletableFuture future;
- protected volatile Flow.Subscription subscription;
- protected volatile String deferredText;
- private final Consumer onComplete;
- private final StringBuilder msgBuilder = new StringBuilder();
-
- public Subscriber(Consumer super ApiResponse> messageDataConsumer, Consumer onComplete) {
- this.messageDataConsumer = messageDataConsumer;
- this.future = new CompletableFuture<>();
- this.subscription = null;
- this.deferredText = null;
- this.onComplete = onComplete;
- }
-
@Override
- public void onSubscribe(Flow.Subscription subscription) {
+ public void onSubscribe(Subscription subscription) {
this.subscription = subscription;
try {
this.deferredText = "";
@@ -66,19 +65,18 @@ public class Subscriber implements HttpResponse.BodySubscriber {
var deferredText = this.deferredText;
for (var buffer : buffers) {
- var s = deferredText + UTF_8.decode(buffer);
- var tokens = s.split("\n\n", -1);
+ var decodedText = deferredText + UTF_8.decode(buffer);
+ var tokens = decodedText.split("\n\n", -1);
for (var i = 0; i < tokens.length - 1; i++) {
- var message = tokens[i];
- var data = extractMessageData(message.split("\n"));
- var choice = data.choices().get(0);
+ var response = extractMessageData(tokens[i].split("\n"));
+ var choice = response.choices().get(0);
if ("stop".equals(choice.finishReason())) {
onComplete();
} else {
msgBuilder.append(choice.text());
}
- this.messageDataConsumer.accept(data);
+ this.messageDataConsumer.accept(response);
}
deferredText = tokens[tokens.length - 1];
}
diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/action/AskAction.java b/src/main/java/ee/carlrobert/chatgpt/ide/action/AskAction.java
index 6eb8770d..b79e01de 100644
--- a/src/main/java/ee/carlrobert/chatgpt/ide/action/AskAction.java
+++ b/src/main/java/ee/carlrobert/chatgpt/ide/action/AskAction.java
@@ -18,13 +18,13 @@ public class AskAction extends AnAction {
public void actionPerformed(@NotNull AnActionEvent event) {
var project = event.getProject();
if (project != null) {
- ApiClient.getInstance().clearQueries();
- var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
- var toolWindow = toolWindowService.getToolWindow(project);
- toolWindow.show();
- toolWindow.setTitle("");
- toolWindowService.removeAll();
- toolWindowService.paintLandingView();
+ var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
+ var toolWindow = toolWindowService.getToolWindow(project);
+ ApiClient.getInstance().clearQueries();
+ toolWindow.show();
+ toolWindow.setTitle("");
+ toolWindowService.removeAll();
+ toolWindowService.paintLandingView();
}
}
}
diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/action/BaseAction.java b/src/main/java/ee/carlrobert/chatgpt/ide/action/BaseAction.java
index 12163f33..79959f97 100644
--- a/src/main/java/ee/carlrobert/chatgpt/ide/action/BaseAction.java
+++ b/src/main/java/ee/carlrobert/chatgpt/ide/action/BaseAction.java
@@ -19,9 +19,9 @@ public abstract class BaseAction extends AnAction {
var project = event.getProject();
var editor = event.getData(PlatformDataKeys.EDITOR);
if (editor != null && project != null) {
- ApiClient.getInstance().clearQueries();
var toolWindowService = ApplicationManager.getApplication().getService(ToolWindowService.class);
var selectedText = editor.getSelectionModel().getSelectedText();
+ ApiClient.getInstance().clearQueries();
initToolWindow(toolWindowService.getToolWindow(project));
toolWindowService.removeAll();
toolWindowService.paintUserMessage(selectedText);
diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowService.java b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowService.java
index 1f5bc8e6..ab9c99b6 100644
--- a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowService.java
+++ b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowService.java
@@ -4,6 +4,8 @@ import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconLabe
import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createTextArea;
import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.justifyLeft;
+import com.intellij.ide.ui.LafManager;
+import com.intellij.ide.ui.LafManagerListener;
import com.intellij.openapi.options.ShowSettingsUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel;
@@ -13,12 +15,15 @@ import ee.carlrobert.chatgpt.EmptyCallback;
import ee.carlrobert.chatgpt.client.ApiClient;
import ee.carlrobert.chatgpt.ide.settings.SettingsConfigurable;
import ee.carlrobert.chatgpt.ide.settings.SettingsState;
+import ee.carlrobert.chatgpt.ide.toolwindow.components.SyntaxTextArea;
import icons.Icons;
import java.awt.Cursor;
import java.awt.GridBagLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
import javax.annotation.Nullable;
import javax.swing.Box;
import javax.swing.ImageIcon;
@@ -27,10 +32,11 @@ import javax.swing.JPanel;
import javax.swing.SwingConstants;
import org.jetbrains.annotations.NotNull;
-public class ToolWindowService {
+public class ToolWindowService implements LafManagerListener {
private ScrollablePanel scrollablePanel;
private boolean isLandingViewVisible;
+ private static final List textAreas = new ArrayList<>();
public void setScrollablePanel(ScrollablePanel scrollablePanel) {
this.scrollablePanel = scrollablePanel;
@@ -65,15 +71,23 @@ public class ToolWindowService {
});
scrollablePanel.add(justifyLeft(label));
} else {
- var textArea = createTextArea("", false);
+ var textArea = new SyntaxTextArea();
scrollablePanel.add(textArea);
+ textAreas.add(textArea);
- ApiClient.getInstance().getCompletionsAsync(prompt, (message) -> {
- textArea.append(message);
- if (scrollToBottom != null) {
- scrollToBottom.call();
- }
- });
+ var messageCounter = new AtomicInteger(0);
+ ApiClient.getInstance().getCompletionsAsync(
+ prompt,
+ (message) -> {
+ if (messageCounter.getAndIncrement() == 0) {
+ message = message.replace("\n", "");
+ }
+ textArea.append(message);
+ if (scrollToBottom != null) {
+ scrollToBottom.call();
+ }
+ },
+ (finalMessage) -> textArea.displayCopyButton());
}
addSpacing(16);
@@ -111,11 +125,18 @@ public class ToolWindowService {
scrollablePanel.removeAll();
}
- public void addSpacing(int height) {
+ private void addSpacing(int height) {
scrollablePanel.add(Box.createVerticalStrut(height));
}
- public void addIconLabel(ImageIcon imageIcon, String text) {
+ private void addIconLabel(ImageIcon imageIcon, String text) {
scrollablePanel.add(justifyLeft(createIconLabel(imageIcon, text)));
}
+
+ @Override
+ public void lookAndFeelChanged(@NotNull LafManager source) {
+ for (var textArea : textAreas) {
+ textArea.changeStyleViaThemeXml();
+ }
+ }
}
diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowUtil.java b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowUtil.java
index 095046b0..c629e517 100644
--- a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowUtil.java
+++ b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowUtil.java
@@ -2,9 +2,12 @@ package ee.carlrobert.chatgpt.ide.toolwindow;
import com.intellij.ui.JBColor;
import java.awt.Component;
+import java.awt.Dimension;
import java.awt.Font;
+import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
+import javax.swing.JButton;
import javax.swing.JLabel;
import javax.swing.JTextArea;
@@ -31,6 +34,14 @@ public class ToolWindowUtil {
return iconLabel;
}
+ public static JButton createIconButton(ImageIcon imageIcon) {
+ var button = new JButton(imageIcon);
+ button.setBorder(BorderFactory.createEmptyBorder());
+ button.setContentAreaFilled(false);
+ button.setPreferredSize(new Dimension(imageIcon.getIconWidth(), imageIcon.getIconHeight()));
+ return button;
+ }
+
public static Box justifyLeft(Component component) {
Box box = Box.createHorizontalBox();
box.add(component);
diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/SyntaxTextArea.java b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/SyntaxTextArea.java
new file mode 100644
index 00000000..f0ae0dca
--- /dev/null
+++ b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/SyntaxTextArea.java
@@ -0,0 +1,81 @@
+package ee.carlrobert.chatgpt.ide.toolwindow.components;
+
+import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconButton;
+
+import com.intellij.util.ui.JBUI;
+import com.intellij.util.ui.UIUtil;
+import icons.Icons;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.io.IOException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.swing.JButton;
+import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
+import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
+import org.fife.ui.rsyntaxtextarea.Theme;
+
+public class SyntaxTextArea extends RSyntaxTextArea {
+
+ public SyntaxTextArea() {
+ super("");
+ setStyles();
+ }
+
+ public Matcher getMarkdownMatcher() {
+ return Pattern.compile("`{3}([\\w]*)\\n([\\S\\s]+?)\\n`{3}").matcher(getText());
+ }
+
+ public void displayCopyButton() {
+ if (getMarkdownMatcher().matches()) {
+ ComponentBorder cb = new ComponentBorder(createCopyButton());
+ cb.setAlignment(TOP_ALIGNMENT);
+ cb.install(this);
+ }
+ }
+
+ private void setStyles() {
+ setMargin(JBUI.insets(5));
+ setAntiAliasingEnabled(true);
+ setEnabled(false);
+ setPaintTabLines(false);
+ setHighlightCurrentLine(false);
+ setLineWrap(true);
+ setWrapStyleWord(true);
+ setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_MARKDOWN);
+ changeStyleViaThemeXml();
+ }
+
+ private void copyToClipboard() {
+ var text = getText();
+ var matcher = getMarkdownMatcher();
+ if (matcher.find()) {
+ text = matcher.group(2);
+ }
+
+ StringSelection stringSelection = new StringSelection(text);
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ clipboard.setContents(stringSelection, null);
+ }
+
+ 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 JButton createCopyButton() {
+ var button = createIconButton(Icons.CopyImageIcon);
+ button.addActionListener(e -> {
+ copyToClipboard();
+ button.setIcon(Icons.DoubleTickImageIcon);
+ });
+ return button;
+ }
+}
diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/TextArea.java b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/TextArea.java
index 78d6ed6c..72752d62 100644
--- a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/TextArea.java
+++ b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/TextArea.java
@@ -1,15 +1,16 @@
package ee.carlrobert.chatgpt.ide.toolwindow.components;
+import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconButton;
+
import com.intellij.ui.JBColor;
+import com.intellij.util.ui.JBUI;
import ee.carlrobert.chatgpt.EmptyCallback;
import icons.Icons;
-import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import javax.swing.AbstractAction;
-import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
@@ -19,6 +20,7 @@ public class TextArea extends JTextArea {
public TextArea(EmptyCallback onSubmit, JScrollPane textAreaScrollPane) {
setForeground(JBColor.GRAY);
+ setMargin(JBUI.insets(5));
addFocusListener(getFocusListener());
addSubmitButton(onSubmit, textAreaScrollPane);
@@ -44,11 +46,7 @@ public class TextArea extends JTextArea {
}
private JButton createSubmitButton(ActionListener submitButtonListener) {
- var sendIcon = Icons.SendImageIcon;
- var button = new JButton(sendIcon);
- button.setBorder(BorderFactory.createEmptyBorder());
- button.setContentAreaFilled(false);
- button.setPreferredSize(new Dimension(sendIcon.getIconWidth(), sendIcon.getIconHeight()));
+ var button = createIconButton(Icons.SendImageIcon);
button.addActionListener(submitButtonListener);
return button;
}
diff --git a/src/main/java/icons/Icons.java b/src/main/java/icons/Icons.java
index 2a3208bc..b69bb261 100644
--- a/src/main/java/icons/Icons.java
+++ b/src/main/java/icons/Icons.java
@@ -12,6 +12,8 @@ public class Icons {
public static final ImageIcon SendImageIcon = getImageIcon("/icons/send-icon.png");
public static final ImageIcon SunImageIcon = getImageIcon("/icons/sun-icon.png");
public static final ImageIcon UserImageIcon = getImageIcon("/icons/user-icon.png");
+ public static final ImageIcon CopyImageIcon = getImageIcon("/icons/copy-icon.png");
+ public static final ImageIcon DoubleTickImageIcon = getImageIcon("/icons/double-tick-icon.png");
private static ImageIcon getImageIcon(String path) {
return new ImageIcon(Objects.requireNonNull(Icons.class.getResource(path)));
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 8fbe4778..db3b7e3e 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -1,17 +1,7 @@
-
-
+
ee.carlrobert.chatgpt
-
-
OpenAI - ChatGPT
-
-
Carl-Robert Linnupuu
-
-
ChatGPT as your copilot to level up your developer experience.
This is the perfect assistant for any programmer who wants to improve their coding skills and make more efficient use of the time.
@@ -31,6 +21,7 @@
+ 1.0.9 Add code syntax highlighting and copy functionality, remove plugin required for restart
1.0.8 Migrate to Java 17, fix ToolWindow NPE
1.0.7 Code refactoring, add readme
1.0.6 Display proper message for insufficient quota
@@ -46,6 +37,12 @@
com.intellij.modules.platform
+
+
+
+
diff --git a/src/main/resources/icons/copy-icon.png b/src/main/resources/icons/copy-icon.png
new file mode 100644
index 00000000..be16c51b
Binary files /dev/null and b/src/main/resources/icons/copy-icon.png differ
diff --git a/src/main/resources/icons/double-tick-icon.png b/src/main/resources/icons/double-tick-icon.png
new file mode 100644
index 00000000..8155abe7
Binary files /dev/null and b/src/main/resources/icons/double-tick-icon.png differ