From 04c2ee1f095992e9e2490f87e7ea033e4fe6e68d Mon Sep 17 00:00:00 2001 From: Carl-Robert Linnupuu Date: Tue, 28 Feb 2023 18:43:17 +0000 Subject: [PATCH] 1.1.3 - Improve error handling, add block caret --- build.gradle.kts | 2 +- .../ee/carlrobert/chatgpt/EmptyCallback.java | 6 --- .../ee/carlrobert/chatgpt/client/Client.java | 8 +-- .../carlrobert/chatgpt/client/Subscriber.java | 35 ++++-------- .../client/chatgpt/ChatGPTBodySubscriber.java | 53 ++++++++++++++----- .../chatgpt/client/chatgpt/ChatGPTClient.java | 23 ++++---- .../chatgpt/response/ChatGPTResponse.java | 4 ++ .../response/ChatGPTResponseDetail.java | 16 ++++++ .../response/ChatGPTResponseMessage.java | 9 ++++ .../ChatGPTResponseMessageAuthor.java | 17 ++++++ .../chatgpt/client/gpt/GPTBodySubscriber.java | 28 +++++++--- .../chatgpt/client/gpt/GPTClient.java | 43 +++++++++------ .../ide/settings/SettingsComponent.java | 2 +- .../ide/toolwindow/ToolWindowService.java | 17 +++--- .../toolwindow/components/SyntaxTextArea.java | 25 +++++++++ .../ide/toolwindow/components/TextArea.java | 9 ++-- src/main/resources/META-INF/plugin.xml | 1 + 17 files changed, 193 insertions(+), 105 deletions(-) delete mode 100644 src/main/java/ee/carlrobert/chatgpt/EmptyCallback.java create mode 100644 src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseDetail.java create mode 100644 src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessageAuthor.java diff --git a/build.gradle.kts b/build.gradle.kts index fc493e66..eae8b7fc 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,7 +4,7 @@ plugins { } group = "ee.carlrobert" -version = "1.1.2" +version = "1.1.3" repositories { mavenCentral() diff --git a/src/main/java/ee/carlrobert/chatgpt/EmptyCallback.java b/src/main/java/ee/carlrobert/chatgpt/EmptyCallback.java deleted file mode 100644 index f735f829..00000000 --- a/src/main/java/ee/carlrobert/chatgpt/EmptyCallback.java +++ /dev/null @@ -1,6 +0,0 @@ -package ee.carlrobert.chatgpt; - -@FunctionalInterface -public interface EmptyCallback { - void call(); -} diff --git a/src/main/java/ee/carlrobert/chatgpt/client/Client.java b/src/main/java/ee/carlrobert/chatgpt/client/Client.java index d48f40c2..d14f708d 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/Client.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/Client.java @@ -2,7 +2,6 @@ package ee.carlrobert.chatgpt.client; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; -import ee.carlrobert.chatgpt.EmptyCallback; import java.net.URI; import java.net.http.HttpClient; import java.net.http.HttpRequest; @@ -24,12 +23,9 @@ public abstract class Client { protected abstract BodySubscriber subscribe( ResponseInfo responseInfo, Consumer onMessageReceived, - EmptyCallback onComplete); + Runnable onComplete); - public void getCompletionsAsync( - String prompt, - Consumer onMessageReceived, - EmptyCallback onComplete) { + public void getCompletionsAsync(String prompt, Consumer onMessageReceived, Runnable onComplete) { this.userPrompt = prompt; this.client.sendAsync( buildHttpRequest(prompt), diff --git a/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java b/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java index 7281e1bf..406c7eb4 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/Subscriber.java @@ -2,34 +2,25 @@ package ee.carlrobert.chatgpt.client; import static java.nio.charset.StandardCharsets.UTF_8; -import com.fasterxml.jackson.core.JsonProcessingException; import java.net.http.HttpResponse.BodySubscriber; import java.nio.ByteBuffer; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Flow.Subscription; -import java.util.function.Consumer; import java.util.regex.Pattern; public abstract class Subscriber implements BodySubscriber { protected final CompletableFuture future = new CompletableFuture<>(); - private final Consumer responseConsumer; private volatile Subscription subscription; private volatile String deferredText; - protected abstract T deserializePayload(String jsonPayload) throws JsonProcessingException; - protected abstract void onRequestComplete(); - // Overridden from concrete class - protected void processRegularResponse(String response) { - } + protected abstract void onErrorOccurred(); - public Subscriber(Consumer responseConsumer) { - this.responseConsumer = responseConsumer; - } + protected abstract void send(String jsonPayload, String token); public CompletionStage getBody() { return this.future; @@ -52,20 +43,12 @@ public abstract class Subscriber implements BodySubscribe for (var buffer : buffers) { var decodedText = deferredText + UTF_8.decode(buffer); var tokens = decodedText.split("\n\n", -1); - if (tokens.length == 1) { - processRegularResponse(decodedText); - } - - for (var i = 0; i < tokens.length - 1; i++) { - var responsePayload = extractPayload(tokens[i].split("\n")); + for (String token : tokens) { + var responsePayload = extractPayload(token.split("\n")); if ("[DONE]".equals(responsePayload)) { future.complete(null); } else { - try { - this.responseConsumer.accept(deserializePayload(responsePayload)); - } catch (JsonProcessingException e) { - throw new RuntimeException("Couldn't deserialize the payload", e); - } + send(responsePayload, token); } } deferredText = tokens[tokens.length - 1]; @@ -80,7 +63,11 @@ public abstract class Subscriber implements BodySubscribe } public void onError(Throwable e) { - this.future.completeExceptionally(e); + try { + onErrorOccurred(); + } finally { + this.future.completeExceptionally(e); + } } public void onComplete() { @@ -93,7 +80,7 @@ public abstract class Subscriber implements BodySubscribe } private String extractPayload(String[] payload) { - Pattern dataLinePattern = Pattern.compile("^data: ?(.*)$"); + Pattern dataLinePattern = Pattern.compile("^data:\\s*(\\{.*})\\s*$"); var responseBuilder = new StringBuilder(); for (var line : payload) { var matcher = dataLinePattern.matcher(line); diff --git a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTBodySubscriber.java b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTBodySubscriber.java index 77d258d9..b802f6fd 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTBodySubscriber.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTBodySubscriber.java @@ -4,39 +4,64 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import ee.carlrobert.chatgpt.client.Subscriber; import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponse; +import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponseDetail; import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponseError; import java.util.function.Consumer; public class ChatGPTBodySubscriber extends Subscriber { + private final Consumer responseConsumer; private final Consumer onCompleteCallback; - private final Consumer onErrorCallback; private final ObjectMapper objectMapper = new ObjectMapper(); - private ChatGPTResponse lastReceivedResponse; public ChatGPTBodySubscriber( - Consumer responseConsumer, - Consumer onCompleteCallback, - Consumer onErrorCallback) { - super(responseConsumer); + Consumer responseConsumer, + Consumer onCompleteCallback) { + this.responseConsumer = responseConsumer; this.onCompleteCallback = onCompleteCallback; - this.onErrorCallback = onErrorCallback; - } - - protected ChatGPTResponse deserializePayload(String jsonPayload) throws JsonProcessingException { - lastReceivedResponse = objectMapper.readValue(jsonPayload, ChatGPTResponse.class); - return lastReceivedResponse; } protected void onRequestComplete() { onCompleteCallback.accept(lastReceivedResponse); } + protected void onErrorOccurred() { + responseConsumer.accept("Something went wrong. Please try again later."); + } - protected void processRegularResponse(String jsonPayload) { + protected void send(String responsePayload, String token) { + if (!responsePayload.isEmpty()) { + try { + var response = objectMapper.readValue(responsePayload, ChatGPTResponse.class); + var author = response.getMessage().getAuthor(); + if (author != null && "assistant".equals(author.getRole())) { + var message = response.getFullMessage(); + if (lastReceivedResponse != null) { + message = message.replace(lastReceivedResponse.getFullMessage(), ""); + } + lastReceivedResponse = response; + this.responseConsumer.accept(message); + } + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to deserialize the payload", e); + } + } else { + try { + var response = objectMapper.readValue(token, ChatGPTResponseDetail.class); + this.responseConsumer.accept(response.getDetail()); + } catch (JsonProcessingException e) { + tryProcessingErrorResponse(token); + } + } + } + + private void tryProcessingErrorResponse(String jsonPayload) { try { - onErrorCallback.accept(objectMapper.readValue(jsonPayload, ChatGPTResponseError.class)); + var error = objectMapper.readValue(jsonPayload, ChatGPTResponseError.class); + if ("invalid_api_key".equals(error.getDetail().getCode())) { + responseConsumer.accept(error.getDetail().getMessage()); + } future.complete(null); } catch (JsonProcessingException e) { future.completeExceptionally(e); diff --git a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTClient.java b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTClient.java index cca4303f..f67bd27f 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTClient.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/ChatGPTClient.java @@ -1,11 +1,11 @@ package ee.carlrobert.chatgpt.client.chatgpt; -import ee.carlrobert.chatgpt.EmptyCallback; import ee.carlrobert.chatgpt.client.ApiRequestDetails; import ee.carlrobert.chatgpt.client.Client; import ee.carlrobert.chatgpt.client.chatgpt.response.ChatGPTResponse; import ee.carlrobert.chatgpt.ide.settings.SettingsState; -import java.net.http.HttpResponse; +import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -17,7 +17,6 @@ public class ChatGPTClient extends Client { private static ChatGPTClient instance; private static ChatGPTResponse lastReceivedResponse; - private ChatGPTClient() { } @@ -61,24 +60,20 @@ public class ChatGPTClient extends Client { settings.accessToken); } - protected HttpResponse.BodySubscriber subscribe( - HttpResponse.ResponseInfo responseInfo, + protected BodySubscriber subscribe( + ResponseInfo responseInfo, Consumer onMessageReceived, - EmptyCallback onComplete) { + Runnable onComplete) { if (responseInfo.statusCode() == 200) { - return new ChatGPTBodySubscriber(( - response -> onMessageReceived.accept(String.join("", response.getMessage().getContent().getParts()))), + return new ChatGPTBodySubscriber( + onMessageReceived, response -> { lastReceivedResponse = response; - onComplete.call(); - }, - error -> { - if ("invalid_api_key".equals(error.getDetail().getCode())) { - onMessageReceived.accept(error.getDetail().getMessage()); - } + onComplete.run(); }); } else { onMessageReceived.accept("Something went wrong. Please try again later."); + onComplete.run(); throw new RuntimeException(); } } diff --git a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponse.java b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponse.java index 9af435af..54e703d0 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponse.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponse.java @@ -26,4 +26,8 @@ public class ChatGPTResponse implements ApiResponse { public void setConversationId(String conversationId) { this.conversationId = conversationId; } + + public String getFullMessage() { + return String.join("", message.getContent().getParts()); + } } diff --git a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseDetail.java b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseDetail.java new file mode 100644 index 00000000..55fbfd08 --- /dev/null +++ b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseDetail.java @@ -0,0 +1,16 @@ +package ee.carlrobert.chatgpt.client.chatgpt.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatGPTResponseDetail { + private String detail; + + public String getDetail() { + return detail; + } + + public void setDetail(String detail) { + this.detail = detail; + } +} diff --git a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessage.java b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessage.java index dce6c701..a38bd053 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessage.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessage.java @@ -6,6 +6,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; public class ChatGPTResponseMessage { private String id; + private ChatGPTResponseMessageAuthor author; private ChatGPTResponseMessageContent content; public String getId() { @@ -16,6 +17,14 @@ public class ChatGPTResponseMessage { this.id = id; } + public ChatGPTResponseMessageAuthor getAuthor() { + return author; + } + + public void setAuthor(ChatGPTResponseMessageAuthor author) { + this.author = author; + } + public ChatGPTResponseMessageContent getContent() { return content; } diff --git a/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessageAuthor.java b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessageAuthor.java new file mode 100644 index 00000000..b8198826 --- /dev/null +++ b/src/main/java/ee/carlrobert/chatgpt/client/chatgpt/response/ChatGPTResponseMessageAuthor.java @@ -0,0 +1,17 @@ +package ee.carlrobert.chatgpt.client.chatgpt.response; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ChatGPTResponseMessageAuthor { + + private String role; + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} diff --git a/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTBodySubscriber.java b/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTBodySubscriber.java index 1a79a500..901d6bf2 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTBodySubscriber.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTBodySubscriber.java @@ -10,21 +10,33 @@ public class GPTBodySubscriber extends Subscriber { private final Consumer onCompleteCallback; private final StringBuilder messageBuilder = new StringBuilder(); + private final Consumer responseConsumer; public GPTBodySubscriber( - Consumer responseConsumer, + Consumer responseConsumer, Consumer onCompleteCallback) { - super(responseConsumer); + this.responseConsumer = responseConsumer; this.onCompleteCallback = onCompleteCallback; } - protected GPTResponse deserializePayload(String jsonPayload) throws JsonProcessingException { - var response = new ObjectMapper().readValue(jsonPayload, GPTResponse.class); - messageBuilder.append(response.getChoices().get(0).getText()); - return response; - } - protected void onRequestComplete() { onCompleteCallback.accept(messageBuilder.toString()); } + + protected void onErrorOccurred() { + responseConsumer.accept("Something went wrong. Please try again later."); + } + + protected void send(String responsePayload, String token) { + try { + if (!responsePayload.isEmpty()) { + var response = new ObjectMapper().readValue(responsePayload, GPTResponse.class); + var message = response.getChoices().get(0).getText(); + messageBuilder.append(message); + this.responseConsumer.accept(message); + } + } catch (JsonProcessingException e) { + future.completeExceptionally(e); + } + } } diff --git a/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTClient.java b/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTClient.java index e72108c8..75529510 100644 --- a/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTClient.java +++ b/src/main/java/ee/carlrobert/chatgpt/client/gpt/GPTClient.java @@ -2,13 +2,12 @@ package ee.carlrobert.chatgpt.client.gpt; import static java.lang.String.format; -import ee.carlrobert.chatgpt.EmptyCallback; import ee.carlrobert.chatgpt.client.ApiRequestDetails; import ee.carlrobert.chatgpt.client.BaseModel; import ee.carlrobert.chatgpt.client.Client; import ee.carlrobert.chatgpt.ide.settings.SettingsState; -import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodySubscriber; +import java.net.http.HttpResponse.ResponseInfo; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -51,26 +50,40 @@ public class GPTClient extends Client { } protected BodySubscriber subscribe( - HttpResponse.ResponseInfo responseInfo, + ResponseInfo responseInfo, Consumer onMessageReceived, - EmptyCallback onComplete) { + Runnable onComplete) { if (responseInfo.statusCode() == 200) { return new GPTBodySubscriber( - response -> onMessageReceived.accept(response.getChoices().get(0).getText()), + onMessageReceived, finalMsg -> { queries.add(Map.entry(super.userPrompt, finalMsg)); - onComplete.call(); + onComplete.run(); }); - } else if (responseInfo.statusCode() == 401) { - onMessageReceived.accept("Incorrect API key provided.\n" + - "You can find your API key at https://platform.openai.com/account/api-keys."); - throw new IllegalArgumentException(); - } else if (responseInfo.statusCode() == 429) { - onMessageReceived.accept("You exceeded your current quota, please check your plan and billing details."); - throw new RuntimeException("Insufficient quota"); } else { - onMessageReceived.accept("Something went wrong. Please try again later."); - throw new RuntimeException(); + handleError(responseInfo, onMessageReceived, onComplete); + return null; + } + } + + private void handleError( + ResponseInfo responseInfo, + Consumer onMessageReceived, + Runnable onComplete) { + try { + if (responseInfo.statusCode() == 401) { + onMessageReceived.accept("Incorrect API key provided.\n" + + "You can find your API key at https://platform.openai.com/account/api-keys."); + throw new IllegalArgumentException(); + } else if (responseInfo.statusCode() == 429) { + onMessageReceived.accept("You exceeded your current quota, please check your plan and billing details."); + throw new RuntimeException("Insufficient quota"); + } else { + onMessageReceived.accept("Something went wrong. Please try again later."); + throw new RuntimeException(); + } + } finally { + onComplete.run(); } } diff --git a/src/main/java/ee/carlrobert/chatgpt/ide/settings/SettingsComponent.java b/src/main/java/ee/carlrobert/chatgpt/ide/settings/SettingsComponent.java index 3bdfac23..28671f91 100644 --- a/src/main/java/ee/carlrobert/chatgpt/ide/settings/SettingsComponent.java +++ b/src/main/java/ee/carlrobert/chatgpt/ide/settings/SettingsComponent.java @@ -115,7 +115,7 @@ public class SettingsComponent { .addComponent(createFirstSelectionForm()) .addVerticalGap(8) .addComponent(UI.PanelFactory.panel(useChatGPTRadioButton) - .withComment("Slow and free, more suitable for conversational tasks") + .withComment("Slow and free, more suitable for conversational tasks, rate-limited") .createPanel()) .addComponent(createSecondSelectionForm()) .getPanel(); 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 fc6201c7..8ff65552 100644 --- a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowService.java +++ b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/ToolWindowService.java @@ -12,9 +12,7 @@ import com.intellij.openapi.project.Project; import com.intellij.openapi.roots.ui.componentsList.components.ScrollablePanel; import com.intellij.openapi.wm.ToolWindow; import com.intellij.openapi.wm.ToolWindowManager; -import ee.carlrobert.chatgpt.EmptyCallback; import ee.carlrobert.chatgpt.client.ClientFactory; -import ee.carlrobert.chatgpt.client.chatgpt.ChatGPTClient; import ee.carlrobert.chatgpt.ide.settings.SettingsConfigurable; import ee.carlrobert.chatgpt.ide.settings.SettingsState; import ee.carlrobert.chatgpt.ide.toolwindow.components.SyntaxTextArea; @@ -35,9 +33,9 @@ import org.jetbrains.annotations.NotNull; public class ToolWindowService implements LafManagerListener { - private ScrollablePanel scrollablePanel; - private boolean isLandingViewVisible; private static final List textAreas = new ArrayList<>(); + private boolean isLandingViewVisible; + private ScrollablePanel scrollablePanel; public void setScrollablePanel(ScrollablePanel scrollablePanel) { this.scrollablePanel = scrollablePanel; @@ -56,7 +54,7 @@ public class ToolWindowService implements LafManagerListener { scrollablePanel.add(createTextArea(userMessage, true)); } - public void sendMessage(String prompt, Project project, @Nullable EmptyCallback scrollToBottom) { + public void sendMessage(String prompt, Project project, @Nullable Runnable scrollToBottom) { addSpacing(16); addIconLabel(Icons.DefaultImageIcon, "ChatGPT:"); addSpacing(8); @@ -73,16 +71,13 @@ public class ToolWindowService implements LafManagerListener { var client = new ClientFactory().getClient(); client.getCompletionsAsync(prompt, message -> { - if (client instanceof ChatGPTClient) { - textArea.setText(message); - } else { - textArea.append(message); - } + textArea.append(message); if (scrollToBottom != null) { - scrollToBottom.call(); + scrollToBottom.run(); } }, () -> { + textArea.getCaret().setVisible(false); textArea.displayCopyButton(); textArea.enableSelection(); }); 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 index c3af402a..a260e755 100644 --- a/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/SyntaxTextArea.java +++ b/src/main/java/ee/carlrobert/chatgpt/ide/toolwindow/components/SyntaxTextArea.java @@ -10,15 +10,19 @@ import java.awt.datatransfer.Clipboard; import java.awt.datatransfer.StringSelection; import java.io.IOException; import javax.swing.JButton; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; import org.fife.ui.rsyntaxtextarea.SyntaxConstants; import org.fife.ui.rsyntaxtextarea.Theme; +import org.fife.ui.rtextarea.CaretStyle; public class SyntaxTextArea extends RSyntaxTextArea { public SyntaxTextArea() { super(""); setStyles(); + addDocumentListener(); } public void displayCopyButton() { @@ -50,6 +54,8 @@ public class SyntaxTextArea extends RSyntaxTextArea { setPaintTabLines(false); setHighlightCurrentLine(false); setLineWrap(true); + setCaretStyle(0, CaretStyle.BLOCK_STYLE); + getCaret().setVisible(true); setWrapStyleWord(true); setSyntaxEditingStyle(SyntaxConstants.SYNTAX_STYLE_MARKDOWN); changeStyleViaThemeXml(); @@ -69,4 +75,23 @@ public class SyntaxTextArea extends RSyntaxTextArea { }); return button; } + + private void addDocumentListener() { + getDocument().addDocumentListener(new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + setCaretPosition(getText().length()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + setCaretPosition(getText().length()); + } + + @Override + public void changedUpdate(DocumentEvent e) { + setCaretPosition(getText().length()); + } + }); + } } 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 72752d62..6a9bb94c 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 @@ -4,7 +4,6 @@ import static ee.carlrobert.chatgpt.ide.toolwindow.ToolWindowUtil.createIconButt import com.intellij.ui.JBColor; import com.intellij.util.ui.JBUI; -import ee.carlrobert.chatgpt.EmptyCallback; import icons.Icons; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; @@ -18,7 +17,7 @@ import javax.swing.KeyStroke; public class TextArea extends JTextArea { - public TextArea(EmptyCallback onSubmit, JScrollPane textAreaScrollPane) { + public TextArea(Runnable onSubmit, JScrollPane textAreaScrollPane) { setForeground(JBColor.GRAY); setMargin(JBUI.insets(5)); addFocusListener(getFocusListener()); @@ -33,13 +32,13 @@ public class TextArea extends JTextArea { var actions = getActionMap(); actions.put("text-submit", new AbstractAction() { public void actionPerformed(ActionEvent e) { - onSubmit.call(); + onSubmit.run(); } }); } - private void addSubmitButton(EmptyCallback onSubmit, JScrollPane textAreaScrollPane) { - var button = createSubmitButton(e -> onSubmit.call()); + private void addSubmitButton(Runnable onSubmit, JScrollPane textAreaScrollPane) { + var button = createSubmitButton(e -> onSubmit.run()); ComponentBorder cb = new ComponentBorder(button); cb.setAdjustInsets(true); cb.install(textAreaScrollPane); diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index d9cb5111..e91eab53 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -21,6 +21,7 @@ +
  • 1.1.3 Improve error handling, add block caret
  • 1.1.2 Add option to select other language models
  • 1.1.1 Remove startup notification
  • 1.1.0 Add reverse proxy support, fix text selection and copy functionality