1.0.9 - Add markdown code syntax highlighting and copy functionality, remove plugin required for restart

This commit is contained in:
Carl-Robert Linnupuu 2023-02-24 01:33:42 +00:00
parent 31dc6ce6aa
commit d1bd09b89b
13 changed files with 192 additions and 66 deletions

View file

@ -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

View file

@ -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<String> onMessage) {
public void getCompletionsAsync(String userPrompt, Consumer<String> onMessage, @Nullable Consumer<String> 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<Void> subscribe(ResponseInfo responseInfo, String userPrompt, Consumer<String> onMessage) {
private BodySubscriber<Void> subscribe(
ResponseInfo responseInfo,
String userPrompt,
Consumer<String> onMessage,
@Nullable Consumer<String> 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.");

View file

@ -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<Void> {
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<Void> future;
private final Consumer<String> onComplete;
private final StringBuilder msgBuilder = new StringBuilder();
public Subscriber(Consumer<? super ApiResponse> messageDataConsumer, Consumer<String> 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<Void> {
}
}
protected final Consumer<? super ApiResponse> messageDataConsumer;
protected final CompletableFuture<Void> future;
protected volatile Flow.Subscription subscription;
protected volatile String deferredText;
private final Consumer<String> onComplete;
private final StringBuilder msgBuilder = new StringBuilder();
public Subscriber(Consumer<? super ApiResponse> messageDataConsumer, Consumer<String> 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<Void> {
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];
}

View file

@ -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();
}
}
}

View file

@ -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);

View file

@ -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<SyntaxTextArea> 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();
}
}
}

View file

@ -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);

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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)));

View file

@ -1,17 +1,7 @@
<!-- Plugin Configuration File. Read more: https://plugins.jetbrains.com/docs/intellij/plugin-configuration-file.html -->
<idea-plugin require-restart="true">
<idea-plugin>
<id>ee.carlrobert.chatgpt</id>
<!-- Public plugin name should be written in Title Case.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-name -->
<name>OpenAI - ChatGPT</name>
<!-- A displayed Vendor name or Organization ID displayed on the Plugins Page. -->
<vendor email="carlrobertoh@gmail.com" url="https://carlrobert.ee">Carl-Robert Linnupuu</vendor>
<!-- Description of the plugin displayed on the Plugin Page and IDE Plugin Manager.
Simple HTML elements (text formatting, paragraphs, and lists) can be added inside of <![CDATA[ ]]> tag.
Guidelines: https://plugins.jetbrains.com/docs/marketplace/plugin-overview-page.html#plugin-description -->
<description><![CDATA[
<p><b>ChatGPT as your copilot to level up your developer experience.</b></p>
<p>This is the perfect assistant for any programmer who wants to improve their coding skills and make more efficient use of the time.</p>
@ -31,6 +21,7 @@
<change-notes>
<![CDATA[
<ul>
<li><b>1.0.9</b> Add code syntax highlighting and copy functionality, remove plugin required for restart</li>
<li><b>1.0.8</b> Migrate to Java 17, fix ToolWindow NPE</li>
<li><b>1.0.7</b> Code refactoring, add readme</li>
<li><b>1.0.6</b> Display proper message for insufficient quota</li>
@ -46,6 +37,12 @@
<depends>com.intellij.modules.platform</depends>
<applicationListeners>
<listener
class="ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowService"
topic="com.intellij.ide.ui.LafManagerListener"/>
</applicationListeners>
<extensions defaultExtensionNs="com.intellij">
<postStartupActivity implementation="ee.carlrobert.chatgpt.ide.notification.ShowNotificationActivity"/>

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B