mutationList) {
@@ -95,15 +101,7 @@ public void generateUnittestRequest(String testCodeFile, String sourceCodeFile,
llmChatMediator.generateUnittestRequest(testCodeFile, sourceCodeFile, mutationList);
}
- // the mediator is dedicated to talk to the LLM, and when the response is ready, the service will propagate the response to the observers
- // so that on the UI layer, this is the place to notify all the observers
- // the mediator is not responsible for the UI, so it does not know the UI is a Swing UI
- @Override
- public void updateChatResponse(String responseType, String chatResponse) {
- LOG.info("Received chat response: " + chatResponse);
- notifyObservers("CHAT_RESPONSE:" + responseType, chatResponse);
- }
-
+
public void handleChatMessage(String message) {
LOG.info("Received chat message: " + message);
llmChatMediator.setOllamaClient(new OllamaClient(
@@ -121,10 +119,14 @@ public String dryRunGetPrompt(String testClassName, String sourceClassName, List
return llmChatMediator.dryRunGetPrompt(testClassName, sourceClassName, mutations);
}
+ @Override
public void onEventHappen(String eventName, Object eventObj) {
// Handle events from LLMChatMediator if needed
switch (eventName) {
- case "CLEAR_CHAT":
+ case ChatEvent.CHAT_REQUEST:
+ handleChatMessage(eventObj.toString());
+ break;
+ case ChatEvent.CLEAR_CHAT:
this.llmChatMediator.clearChat();
break;
default:
@@ -137,6 +139,6 @@ public String getChatHistory() {
}
public void propagateConfigChange() {
- notifyObservers("CONFIG_CHANGE:copyAsMarkdown", myState.copyAsMarkdown);
+ EventBusService.getInstance().post(new LLMConfigEvent(LLMConfigEvent.CONFIG_CHANGE_COPY_AS_MARKDOWN, myState.copyAsMarkdown));
}
}
\ No newline at end of file
diff --git a/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/system/EventBusService.java b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/system/EventBusService.java
new file mode 100644
index 00000000..f68ffebc
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/system/EventBusService.java
@@ -0,0 +1,51 @@
+package com.github.jaksonlin.testcraft.infrastructure.services.system;
+
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.BaseEvent;
+import com.google.common.eventbus.EventBus;
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.application.ApplicationManager;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Service(Service.Level.APP)
+public final class EventBusService {
+ private final EventBus eventBus;
+ private final Logger logger = LoggerFactory.getLogger(EventBusService.class);
+
+ public EventBusService() {
+ this.eventBus = new EventBus("TestCraftEventBus");
+ }
+
+ public void register(Object subscriber) {
+ try {
+ eventBus.register(subscriber);
+ logger.debug("Registered subscriber: {}", subscriber.getClass().getSimpleName());
+ } catch (Exception e) {
+ logger.error("Failed to register subscriber: " + subscriber, e);
+ }
+ }
+
+ public void unregister(Object subscriber) {
+ try {
+ eventBus.unregister(subscriber);
+ logger.debug("Unregistered subscriber: {}", subscriber.getClass().getSimpleName());
+ } catch (Exception e) {
+ logger.error("Failed to unregister subscriber: " + subscriber, e);
+ }
+ }
+
+ public void post(Object event) {
+ try {
+ if (event instanceof BaseEvent) {
+ logger.debug("Posting event: {}", ((BaseEvent) event).getEventType());
+ }
+ eventBus.post(event);
+ } catch (Exception e) {
+ logger.error("Failed to post event: " + event, e);
+ }
+ }
+
+ public static EventBusService getInstance() {
+ return ApplicationManager.getApplication().getService(EventBusService.class);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/system/I18nService.java b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/system/I18nService.java
new file mode 100644
index 00000000..6c9c3032
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/infrastructure/services/system/I18nService.java
@@ -0,0 +1,28 @@
+package com.github.jaksonlin.testcraft.infrastructure.services.system;
+
+import java.util.ResourceBundle;
+
+import org.jetbrains.annotations.PropertyKey;
+
+import com.intellij.AbstractBundle;
+import com.intellij.openapi.components.Service;
+import com.intellij.openapi.application.ApplicationManager;
+
+@Service(Service.Level.APP)
+public final class I18nService {
+ private final String BUNDLE = "messages.MyBundle";
+ private ResourceBundle ourBundle;
+ public String message(@PropertyKey(resourceBundle = BUNDLE) String key, Object... params) {
+ return AbstractBundle.message(getBundle(), key, params);
+ }
+ private ResourceBundle getBundle() {
+ if (ourBundle == null) {
+ ourBundle = ResourceBundle.getBundle(BUNDLE);
+ }
+ return ourBundle;
+ }
+
+ public static I18nService getInstance() {
+ return ApplicationManager.getApplication().getService(I18nService.class);
+ }
+}
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/settings/AnnotationSettingsComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/AnnotationSettingsComponent.java
similarity index 66%
rename from src/main/java/com/github/jaksonlin/pitestintellij/settings/AnnotationSettingsComponent.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/AnnotationSettingsComponent.java
index b9b2fe01..6c0a7c3c 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/settings/AnnotationSettingsComponent.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/AnnotationSettingsComponent.java
@@ -1,8 +1,7 @@
-package com.github.jaksonlin.pitestintellij.settings;
+package com.github.jaksonlin.testcraft.presentation.components.configuration;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
import com.intellij.json.JsonFileType;
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.editor.EditorFactory;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.ScrollType;
import com.intellij.openapi.editor.ex.EditorEx;
@@ -10,10 +9,8 @@
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBTextField;
import org.jetbrains.annotations.NotNull;
-import com.github.jaksonlin.pitestintellij.MyBundle;
import javax.swing.*;
-import javax.swing.event.HyperlinkEvent;
import java.awt.*;
public class AnnotationSettingsComponent {
@@ -23,7 +20,7 @@ public class AnnotationSettingsComponent {
private final JCheckBox autoImportCheckBox;
private final JCheckBox enableValidationCheckBox;
private static final int EDITOR_HEIGHT = 300;
-
+
private static final String EXAMPLE_JSON = "{\n" +
" \"fields\": [\n" +
" {\n" +
@@ -99,29 +96,29 @@ public AnnotationSettingsComponent() {
c.gridx = 0;
c.gridy = 0;
c.weighty = 0.0;
- JLabel importSettingsLabel = new JBLabel("" + MyBundle.message("settings.annotation.import.title") + "");
+ JLabel importSettingsLabel = new JBLabel("" + I18nService.getInstance().message("settings.annotation.import.title") + "");
contentPanel.add(importSettingsLabel, c);
// Package input
- addLabelAndField(contentPanel, MyBundle.message("settings.annotation.package.label"), packageTextField, 1,
- MyBundle.message("settings.annotation.package.tooltip"));
+ addLabelAndField(contentPanel, I18nService.getInstance().message("settings.annotation.package.label"), packageTextField, 1,
+ I18nService.getInstance().message("settings.annotation.package.tooltip"));
// Checkboxes
c.gridy = 2;
c.gridwidth = 2;
- autoImportCheckBox = new JCheckBox(MyBundle.message("settings.annotation.autoImport"));
- autoImportCheckBox.setToolTipText(MyBundle.message("settings.annotation.autoImport.tooltip"));
+ autoImportCheckBox = new JCheckBox(I18nService.getInstance().message("settings.annotation.autoImport"));
+ autoImportCheckBox.setToolTipText(I18nService.getInstance().message("settings.annotation.autoImport.tooltip"));
contentPanel.add(autoImportCheckBox, c);
c.gridy = 3;
- enableValidationCheckBox = new JCheckBox(MyBundle.message("settings.annotation.enableValidation"));
- enableValidationCheckBox.setToolTipText(MyBundle.message("settings.annotation.enableValidation.tooltip"));
+ enableValidationCheckBox = new JCheckBox(I18nService.getInstance().message("settings.annotation.enableValidation"));
+ enableValidationCheckBox.setToolTipText(I18nService.getInstance().message("settings.annotation.enableValidation.tooltip"));
contentPanel.add(enableValidationCheckBox, c);
// Schema Configuration section
c.gridy = 4;
c.insets = new Insets(15, 5, 5, 5);
- JLabel schemaLabel = new JBLabel("" + MyBundle.message("settings.annotation.schema.title") + "");
+ JLabel schemaLabel = new JBLabel("" + I18nService.getInstance().message("settings.annotation.schema.title") + "");
contentPanel.add(schemaLabel, c);
// Schema editor
@@ -132,7 +129,7 @@ public AnnotationSettingsComponent() {
// Wrap editor in a panel to ensure it expands properly
JPanel editorPanel = new JPanel(new BorderLayout());
- editorPanel.add(new JBLabel(MyBundle.message("settings.annotation.schema.label")), BorderLayout.NORTH);
+ editorPanel.add(new JBLabel(I18nService.getInstance().message("settings.annotation.schema.label")), BorderLayout.NORTH);
editorPanel.add(schemaEditor, BorderLayout.CENTER);
contentPanel.add(editorPanel, c);
@@ -145,55 +142,33 @@ public AnnotationSettingsComponent() {
JPanel helpPanel = new JPanel(new BorderLayout());
helpPanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
- JLabel helpText = new JBLabel("" +
- "" + MyBundle.message("settings.annotation.schema.help.title") + "
" +
- "" + MyBundle.message("settings.annotation.schema.help.intro") + "
" +
-
- "" + MyBundle.message("settings.annotation.schema.help.structure.title") + "
" +
- "" +
- "- " + MyBundle.message("settings.annotation.schema.help.structure.1") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.structure.2") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.structure.3") + "
" +
- "
" +
+ JLabel helpText = new JBLabel("" +
+ "" + I18nService.getInstance().message("settings.annotation.schema.help.title") + "
" +
+ "" + I18nService.getInstance().message("settings.annotation.schema.help.intro") + "
" +
- "" + MyBundle.message("settings.annotation.schema.help.validation.title") + "
" +
+ "" + I18nService.getInstance().message("settings.annotation.schema.help.structure.title") + "
" +
"" +
- "- " + MyBundle.message("settings.annotation.schema.help.validation.1") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.validation.2") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.validation.3") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.validation.4") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.validation.5") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.structure.1") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.structure.2") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.structure.3") + "
" +
"
" +
- "" + MyBundle.message("settings.annotation.schema.help.valueProvider.title") + "
" +
+ "" + I18nService.getInstance().message("settings.annotation.schema.help.validation.title") + "
" +
"" +
- "- " + MyBundle.message("settings.annotation.schema.help.valueProvider.1") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.valueProvider.2") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.valueProvider.3") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.validation.1") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.validation.2") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.validation.3") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.validation.4") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.validation.5") + "
" +
"
" +
- "" + MyBundle.message("settings.annotation.schema.help.example.title") + "
" +
- "" +
- EXAMPLE_JSON.replace("<", "<").replace(">", ">").replace("\n", "
").replace(" ", " ") +
- "
" +
-
- "" + MyBundle.message("settings.annotation.schema.help.notes.title") + "
" +
+ "" + I18nService.getInstance().message("settings.annotation.schema.help.valueProvider.title") + "
" +
"" +
- "- " + MyBundle.message("settings.annotation.schema.help.notes.1") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.notes.2") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.notes.3") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.notes.4") + "
" +
- "- " + MyBundle.message("settings.annotation.schema.help.notes.5") + "
" +
+ "- " + I18nService.getInstance().message("settings.annotation.schema.help.valueProvider.1") + "
" +
"
" +
- "");
-
- // Create a scroll pane for the help text
- JScrollPane scrollPane = new JScrollPane(helpText);
- scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
- scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
- scrollPane.setBorder(BorderFactory.createEmptyBorder());
+ "");
- helpPanel.add(scrollPane, BorderLayout.CENTER);
+ helpPanel.add(helpText, BorderLayout.CENTER);
contentPanel.add(helpPanel, c);
// Add content panel to main panel
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/settings/InvalidTestCaseSettingsComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/InvalidTestCaseSettingsComponent.java
similarity index 74%
rename from src/main/java/com/github/jaksonlin/pitestintellij/settings/InvalidTestCaseSettingsComponent.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/InvalidTestCaseSettingsComponent.java
index abcf0e7c..9b58ed9e 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/settings/InvalidTestCaseSettingsComponent.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/InvalidTestCaseSettingsComponent.java
@@ -1,12 +1,13 @@
-package com.github.jaksonlin.pitestintellij.settings;
+package com.github.jaksonlin.testcraft.presentation.components.configuration;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
import com.intellij.openapi.editor.EditorSettings;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.ui.EditorTextField;
import com.intellij.ui.components.JBCheckBox;
import com.intellij.ui.components.JBLabel;
import org.jetbrains.annotations.NotNull;
-import com.github.jaksonlin.pitestintellij.MyBundle;
+
import javax.swing.*;
import java.awt.*;
@@ -36,30 +37,30 @@ public InvalidTestCaseSettingsComponent() {
c.gridx = 0;
c.gridy = 0;
c.weighty = 0.0;
- JLabel validationLabel = new JBLabel("" + MyBundle.message("settings.invalidTestCase.title") + "");
+ JLabel validationLabel = new JBLabel("" + I18nService.getInstance().message("settings.invalidTestCase.title") + "");
contentPanel.add(validationLabel, c);
// Checkboxes
c.gridy = 1;
- enableCheckbox = new JBCheckBox(MyBundle.message("settings.invalidTestCase.enableCheck"));
- enableCheckbox.setToolTipText(MyBundle.message("settings.invalidTestCase.enableCheck.tooltip"));
+ enableCheckbox = new JBCheckBox(I18nService.getInstance().message("settings.invalidTestCase.enableCheck"));
+ enableCheckbox.setToolTipText(I18nService.getInstance().message("settings.invalidTestCase.enableCheck.tooltip"));
contentPanel.add(enableCheckbox, c);
c.gridy = 2;
- enableCommentCheckbox = new JBCheckBox(MyBundle.message("settings.invalidTestCase.enableCommentCheck"));
- enableCommentCheckbox.setToolTipText(MyBundle.message("settings.invalidTestCase.enableCommentCheck.tooltip"));
+ enableCommentCheckbox = new JBCheckBox(I18nService.getInstance().message("settings.invalidTestCase.enableCommentCheck"));
+ enableCommentCheckbox.setToolTipText(I18nService.getInstance().message("settings.invalidTestCase.enableCommentCheck.tooltip"));
contentPanel.add(enableCommentCheckbox, c);
// Invalid Assertions section
c.gridy = 3;
c.insets = new Insets(15, 5, 5, 5);
- JLabel assertionsLabel = new JBLabel("" + MyBundle.message("settings.invalidTestCase.assertions.title") + "");
+ JLabel assertionsLabel = new JBLabel("" + I18nService.getInstance().message("settings.invalidTestCase.assertions.title") + "");
contentPanel.add(assertionsLabel, c);
// Editor description
c.gridy = 4;
c.insets = new Insets(5, 5, 5, 5);
- JLabel editorDesc = new JBLabel(MyBundle.message("settings.invalidTestCase.assertions.description"));
+ JLabel editorDesc = new JBLabel(I18nService.getInstance().message("settings.invalidTestCase.assertions.description"));
contentPanel.add(editorDesc, c);
// Assertion editor
@@ -73,11 +74,11 @@ public InvalidTestCaseSettingsComponent() {
c.weighty = 0.0; // Reset vertical weight
c.insets = new Insets(10, 5, 5, 5);
JLabel helpText = new JBLabel("" +
- "" + MyBundle.message("settings.invalidTestCase.assertions.examples.title") + "
" +
- MyBundle.message("settings.invalidTestCase.assertions.examples.1") + "
" +
- MyBundle.message("settings.invalidTestCase.assertions.examples.2") + "
" +
- MyBundle.message("settings.invalidTestCase.assertions.examples.3") + "
" +
- MyBundle.message("settings.invalidTestCase.assertions.examples.4") +
+ "" + I18nService.getInstance().message("settings.invalidTestCase.assertions.examples.title") + "
" +
+ I18nService.getInstance().message("settings.invalidTestCase.assertions.examples.1") + "
" +
+ I18nService.getInstance().message("settings.invalidTestCase.assertions.examples.2") + "
" +
+ I18nService.getInstance().message("settings.invalidTestCase.assertions.examples.3") + "
" +
+ I18nService.getInstance().message("settings.invalidTestCase.assertions.examples.4") +
"");
contentPanel.add(helpText, c);
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/settings/OllamaSettingsComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/OllamaSettingsComponent.java
similarity index 68%
rename from src/main/java/com/github/jaksonlin/pitestintellij/settings/OllamaSettingsComponent.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/OllamaSettingsComponent.java
index 09f2f507..3fbfb9f3 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/settings/OllamaSettingsComponent.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/configuration/OllamaSettingsComponent.java
@@ -1,15 +1,17 @@
-package com.github.jaksonlin.pitestintellij.settings;
+package com.github.jaksonlin.testcraft.presentation.components.configuration;
import com.intellij.ui.components.JBLabel;
import com.intellij.ui.components.JBTextField;
import com.intellij.util.ui.JBUI;
import org.jetbrains.annotations.NotNull;
-import com.github.jaksonlin.pitestintellij.util.OllamaClient;
-import com.github.jaksonlin.pitestintellij.MyBundle;
+import com.github.jaksonlin.testcraft.util.OllamaClient;
+
import javax.swing.*;
import java.awt.*;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
+
public class OllamaSettingsComponent {
private final JPanel mainPanel;
private final JBTextField hostField = new JBTextField();
@@ -30,49 +32,49 @@ public OllamaSettingsComponent() {
contentPanel.setLayout(new BoxLayout(contentPanel, BoxLayout.Y_AXIS));
// Connection Settings Section
- JPanel connectionPanel = createSectionPanel(MyBundle.message("llm.settings.connection.title"));
+ JPanel connectionPanel = createSectionPanel(I18nService.getInstance().message("llm.settings.connection.title"));
GridBagConstraints c = new GridBagConstraints();
c.insets = JBUI.insets(5);
c.fill = GridBagConstraints.HORIZONTAL;
c.anchor = GridBagConstraints.LINE_START;
// Host field
- addLabelAndField(connectionPanel, MyBundle.message("llm.settings.host.label"), hostField,
- MyBundle.message("llm.settings.host.tooltip"));
+ addLabelAndField(connectionPanel, I18nService.getInstance().message("llm.settings.host.label"), hostField,
+ I18nService.getInstance().message("llm.settings.host.tooltip"));
// Port field
- addLabelAndField(connectionPanel, MyBundle.message("llm.settings.port.label"), portField,
- MyBundle.message("llm.settings.port.tooltip"));
+ addLabelAndField(connectionPanel, I18nService.getInstance().message("llm.settings.port.label"), portField,
+ I18nService.getInstance().message("llm.settings.port.tooltip"));
contentPanel.add(connectionPanel);
contentPanel.add(Box.createVerticalStrut(10));
// Model Settings Section
- JPanel modelPanel = createSectionPanel(MyBundle.message("llm.settings.model.title"));
+ JPanel modelPanel = createSectionPanel(I18nService.getInstance().message("llm.settings.model.title"));
// Model field
- addLabelAndField(modelPanel, MyBundle.message("llm.settings.model.label"), modelField,
- MyBundle.message("llm.settings.model.tooltip"));
+ addLabelAndField(modelPanel, I18nService.getInstance().message("llm.settings.model.label"), modelField,
+ I18nService.getInstance().message("llm.settings.model.tooltip"));
// Max Tokens field
- addLabelAndField(modelPanel, MyBundle.message("llm.settings.maxTokens.label"), maxTokensField,
- MyBundle.message("llm.settings.maxTokens.tooltip"));
+ addLabelAndField(modelPanel, I18nService.getInstance().message("llm.settings.maxTokens.label"), maxTokensField,
+ I18nService.getInstance().message("llm.settings.maxTokens.tooltip"));
// Temperature field
- addLabelAndField(modelPanel, MyBundle.message("llm.settings.temperature.label"), temperatureField,
- MyBundle.message("llm.settings.temperature.tooltip"));
+ addLabelAndField(modelPanel, I18nService.getInstance().message("llm.settings.temperature.label"), temperatureField,
+ I18nService.getInstance().message("llm.settings.temperature.tooltip"));
// Timeout field
- addLabelAndField(modelPanel, MyBundle.message("llm.settings.timeout.label"), timeoutField,
- MyBundle.message("llm.settings.timeout.tooltip"));
+ addLabelAndField(modelPanel, I18nService.getInstance().message("llm.settings.timeout.label"), timeoutField,
+ I18nService.getInstance().message("llm.settings.timeout.tooltip"));
contentPanel.add(modelPanel);
contentPanel.add(Box.createVerticalStrut(10));
// Output Settings Section
- JPanel outputPanel = createSectionPanel(MyBundle.message("llm.settings.output.title"));
- copyAsMarkdownCheckbox = new JCheckBox(MyBundle.message("llm.settings.copyMarkdown.label"));
- copyAsMarkdownCheckbox.setToolTipText(MyBundle.message("llm.settings.copyMarkdown.tooltip"));
+ JPanel outputPanel = createSectionPanel(I18nService.getInstance().message("llm.settings.output.title"));
+ copyAsMarkdownCheckbox = new JCheckBox(I18nService.getInstance().message("llm.settings.copyMarkdown.label"));
+ copyAsMarkdownCheckbox.setToolTipText(I18nService.getInstance().message("llm.settings.copyMarkdown.tooltip"));
copyAsMarkdownCheckbox.setAlignmentX(Component.LEFT_ALIGNMENT);
outputPanel.add(copyAsMarkdownCheckbox);
@@ -80,18 +82,18 @@ public OllamaSettingsComponent() {
contentPanel.add(Box.createVerticalStrut(10));
// Test Connection Section
- JPanel testPanel = createSectionPanel(MyBundle.message("llm.settings.test.title"));
- JButton testConnectionButton = new JButton(MyBundle.message("llm.settings.test.button"));
+ JPanel testPanel = createSectionPanel(I18nService.getInstance().message("llm.settings.test.title"));
+ JButton testConnectionButton = new JButton(I18nService.getInstance().message("llm.settings.test.button"));
testConnectionButton.setAlignmentX(Component.LEFT_ALIGNMENT);
testPanel.add(testConnectionButton);
// Add help text
JLabel helpText = new JBLabel("" +
- "" + MyBundle.message("llm.settings.help.title") + "
" +
+ "" + I18nService.getInstance().message("llm.settings.help.title") + "
" +
"" +
- "- " + MyBundle.message("llm.settings.help.running") + "
" +
- "- " + MyBundle.message("llm.settings.help.host") + "
" +
- "- " + MyBundle.message("llm.settings.help.port") + "
" +
+ "- " + I18nService.getInstance().message("llm.settings.help.running") + "
" +
+ "- " + I18nService.getInstance().message("llm.settings.help.host") + "
" +
+ "- " + I18nService.getInstance().message("llm.settings.help.port") + "
" +
"
" +
"");
helpText.setAlignmentX(Component.LEFT_ALIGNMENT);
@@ -152,18 +154,18 @@ private void testConnection() {
if (success) {
JOptionPane.showMessageDialog(mainPanel,
"Successfully connected to Ollama server!",
- MyBundle.message("llm.settings.test.title"),
+ I18nService.getInstance().message("llm.settings.test.title"),
JOptionPane.INFORMATION_MESSAGE);
} else {
JOptionPane.showMessageDialog(mainPanel,
- MyBundle.message("llm.error.connection"),
- MyBundle.message("llm.settings.test.title"),
+ I18nService.getInstance().message("llm.error.connection"),
+ I18nService.getInstance().message("llm.settings.test.title"),
JOptionPane.ERROR_MESSAGE);
}
} catch (Exception e) {
JOptionPane.showMessageDialog(mainPanel,
- MyBundle.message("llm.error.connection") + ": " + e.getMessage(),
- MyBundle.message("llm.settings.test.title"),
+ I18nService.getInstance().message("llm.error.connection") + ": " + e.getMessage(),
+ I18nService.getInstance().message("llm.settings.test.title"),
JOptionPane.ERROR_MESSAGE);
}
}
diff --git a/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/ChatPanelComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/ChatPanelComponent.java
new file mode 100644
index 00000000..a0c32568
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/ChatPanelComponent.java
@@ -0,0 +1,102 @@
+package com.github.jaksonlin.testcraft.presentation.components.llmchat;
+
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.ChatEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.TypedEventObserver;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.EventBusService;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.util.ui.JBUI;
+
+import javax.swing.JButton;
+import javax.swing.JPanel;
+import javax.swing.JTextArea;
+import java.awt.BorderLayout;
+import java.awt.event.KeyEvent;
+import java.awt.event.KeyAdapter;
+
+public class ChatPanelComponent {
+
+ private final TypedEventObserver chatObserver = new TypedEventObserver(ChatEvent.class) {
+ @Override
+ protected void onTypedEvent(ChatEvent event) {
+ // Handle the chat event
+ switch (event.getEventType()) {
+ case ChatEvent.START_LOADING:
+ // disable input area and send button
+ setInputEnabled(false);
+ break;
+ case ChatEvent.STOP_LOADING:
+ case ChatEvent.CHAT_RESPONSE:
+ case ChatEvent.ERROR:
+ // enable input area and send button
+ setInputEnabled(true);
+ break;
+ }
+ }
+ };
+ private final JTextArea inputArea;
+ private final JButton sendButton;
+ private final JPanel masterPanel;
+
+ public ChatPanelComponent() {
+
+ // Input area
+ inputArea = new JTextArea(3, 40);
+ inputArea.setLineWrap(true);
+ inputArea.setWrapStyleWord(true);
+ JBScrollPane inputScrollPane = new JBScrollPane(inputArea);
+
+ // Send button
+ sendButton = new JButton(I18nService.getInstance().message("chat.send.button"));
+ sendButton.addActionListener(e -> sendMessage());
+
+ // Input panel (input area + send button)
+ masterPanel = new JPanel(new BorderLayout());
+ masterPanel.setBorder(JBUI.Borders.empty(5));
+ masterPanel.add(inputScrollPane, BorderLayout.CENTER);
+ masterPanel.add(sendButton, BorderLayout.EAST);
+
+ // Add key listener for Ctrl+Enter to send
+ inputArea.addKeyListener(new KeyAdapter() {
+ @Override
+ public void keyPressed(KeyEvent e) {
+ if (e.getKeyCode() == KeyEvent.VK_ENTER && e.isControlDown()) {
+ sendMessage();
+ e.consume();
+ }
+ }
+ });
+
+
+ }
+
+
+ public JPanel getInputPanel() {
+ return masterPanel;
+ }
+
+ private void sendMessage() {
+ String message = inputArea.getText().trim();
+ if (!message.isEmpty()) {
+ EventBusService.getInstance().post(new ChatEvent(ChatEvent.CHAT_REQUEST, message));
+ inputArea.setText("");
+ inputArea.requestFocus();
+ }
+ }
+
+ public void clear() {
+ inputArea.setText("");
+ }
+
+ public void setInputEnabled(boolean enabled) {
+ inputArea.setEnabled(enabled);
+ sendButton.setEnabled(enabled);
+ if (enabled) {
+ inputArea.requestFocus();
+ }
+ }
+
+ public void dispose() {
+ chatObserver.unregister();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/LLMResponseComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/LLMResponseComponent.java
new file mode 100644
index 00000000..49451490
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/LLMResponseComponent.java
@@ -0,0 +1,341 @@
+package com.github.jaksonlin.testcraft.presentation.components.llmchat;
+
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.ChatEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.LLMConfigEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.TypedEventObserver;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.EventBusService;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
+import com.intellij.ui.components.JBScrollPane;
+import com.intellij.util.ui.JBUI;
+import org.commonmark.node.Node;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import com.intellij.ui.JBColor;
+import java.awt.datatransfer.StringSelection;
+import java.awt.Toolkit;
+
+import javax.swing.*;
+import javax.swing.text.html.HTMLEditorKit;
+import javax.swing.text.html.StyleSheet;
+import java.awt.*;
+import java.util.Timer;
+import java.util.TimerTask;
+import javax.swing.text.html.HTMLDocument;
+
+public class LLMResponseComponent {
+ private final TypedEventObserver eventObserver = new TypedEventObserver(ChatEvent.class) {
+ @Override
+ public void onTypedEvent(ChatEvent event) {
+ switch (event.getEventType()) {
+ case ChatEvent.START_LOADING:
+ startLoading();
+ break;
+ case ChatEvent.STOP_LOADING:
+ stopLoading();
+ break;
+ case ChatEvent.CHAT_REQUEST:
+ if (!isLoading) {
+ appendMarkdownToOutput(String.format(MESSAGE_TEMPLATE, "user", I18nService.getInstance().message("llm.user"), event.getPayload().toString()));
+ startLoading();
+ }
+ break;
+ case ChatEvent.CHAT_RESPONSE:
+ stopLoading();
+ appendMarkdownToOutput(String.format(MESSAGE_TEMPLATE, "assistant", I18nService.getInstance().message("llm.assistant"), event.getPayload().toString()));
+ break;
+ case ChatEvent.COPY_CHAT_RESPONSE:
+ copyToClipboard(event.getPayload());
+ break;
+ case ChatEvent.DRY_RUN_PROMPT:
+ String dryRunPrompt = (String) event.getPayload();
+ if (dryRunPrompt.isEmpty()){
+ JOptionPane.showMessageDialog(masterPanel, I18nService.getInstance().message("llm.dry.run.prompt.empty"), I18nService.getInstance().message("llm.dry.run.prompt"), JOptionPane.INFORMATION_MESSAGE);
+ } else {
+ appendMarkdownToOutput(String.format(MESSAGE_TEMPLATE, "system", I18nService.getInstance().message("llm.system"), I18nService.getInstance().message("llm.dry.run.prompt") + "\n" + event.getPayload().toString()));
+ }
+ break;
+ case ChatEvent.ERROR:
+ JOptionPane.showMessageDialog(masterPanel, I18nService.getInstance().message("llm.error") + ": " + event.getPayload().toString(), I18nService.getInstance().message("llm.error"), JOptionPane.ERROR_MESSAGE);
+ break;
+ }
+ }
+ };
+
+ private final TypedEventObserver llmConfigObserver = new TypedEventObserver(LLMConfigEvent.class) {
+ @Override
+ public void onTypedEvent(LLMConfigEvent event) {
+ switch (event.getEventType()) {
+ case LLMConfigEvent.CONFIG_CHANGE_COPY_AS_MARKDOWN:
+ copyAsMarkdown = (boolean) event.getPayload();
+ break;
+ }
+ }
+ };
+ private final JEditorPane outputArea;
+ private boolean isLoading = false;
+ private boolean copyAsMarkdown = false;
+
+ private final HTMLEditorKit htmlKit;
+ private final StyleSheet styleSheet;
+ private final JPanel masterPanel;
+ private final JPanel loadingPanel;
+ private final Timer loadingTimer;
+ private final JLabel loadingLabel;
+
+
+ private final StringBuilder chatHistory = new StringBuilder();
+
+ public JPanel getMasterPanel() {
+ return masterPanel;
+ }
+
+
+ private static final String BASE_HTML_TEMPLATE =
+ "\n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ "\n" +
+ "\n" +
+ " \n" +
+ " %s\n" +
+ "
\n" +
+ "\n" +
+ "";
+
+ private static final String MESSAGE_TEMPLATE =
+ "\n" +
+ " \n" +
+ "
%s
\n" +
+ "
\n" +
+ "";
+
+
+ public void notifyClearButtonClick() {
+ EventBusService.getInstance().post(new ChatEvent(ChatEvent.CLEAR_CHAT, null));
+ }
+
+ public void notifyCopyButtonClick() {
+ EventBusService.getInstance().post(new ChatEvent(ChatEvent.REQUEST_COPY_CHAT_RESPONSE, null));
+ }
+
+ public LLMResponseComponent(ChatPanelComponent chatPanelComponent) {
+ masterPanel = new JPanel(new BorderLayout());
+ masterPanel.setBorder(JBUI.Borders.empty(10));
+
+
+ // Setup improved JEditorPane for HTML rendering
+ outputArea = new JEditorPane();
+ outputArea.setEditable(false);
+ outputArea.setContentType("text/html");
+
+ // Configure HTML editor kit with custom style sheet
+ htmlKit = new HTMLEditorKit();
+ styleSheet = htmlKit.getStyleSheet();
+ styleSheet.addRule(getCodeStyle());
+ outputArea.setEditorKit(htmlKit);
+
+ // Initialize with empty document
+ HTMLDocument doc = (HTMLDocument) htmlKit.createDefaultDocument();
+ outputArea.setDocument(doc);
+
+ // Enable proper HTML rendering
+ outputArea.putClientProperty(JEditorPane.HONOR_DISPLAY_PROPERTIES, Boolean.TRUE);
+ outputArea.setFont(new Font(Font.SANS_SERIF, Font.PLAIN, 13));
+
+ // Create loading panel
+ loadingPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
+ loadingPanel.setVisible(false);
+ loadingLabel = new JLabel(I18nService.getInstance().message("llm.thinking"));
+ loadingPanel.add(loadingLabel);
+ loadingPanel.setBorder(BorderFactory.createEmptyBorder(5, 10, 5, 10));
+ loadingPanel.setBackground(JBColor.background());
+
+ // Setup loading animation timer
+ loadingTimer = new Timer();
+
+ JBScrollPane outputScrollPane = new JBScrollPane(outputArea);
+
+ // Create toolbar
+ JToolBar toolbar = new JToolBar();
+ toolbar.setFloatable(false);
+ toolbar.setBorder(JBUI.Borders.empty(2, 2));
+
+ JButton copyButton = new JButton(I18nService.getInstance().message("llm.copy.to.clipboard"));
+ copyButton.addActionListener(e -> {
+ notifyCopyButtonClick();
+ });
+ toolbar.add(copyButton);
+
+ JButton clearButton = new JButton(I18nService.getInstance().message("llm.clear"));
+ clearButton.addActionListener(e -> {
+ clearOutput();
+ notifyClearButtonClick();
+ });
+ toolbar.add(clearButton);
+
+ // Create input panel at the bottom
+ JPanel inputPanel = new JPanel(new BorderLayout());
+ inputPanel.add(chatPanelComponent.getInputPanel(), BorderLayout.CENTER);
+
+ // Create center panel to hold toolbar, output area, and loading panel
+ JPanel centerPanel = new JPanel(new BorderLayout());
+ centerPanel.add(toolbar, BorderLayout.NORTH);
+ centerPanel.add(outputScrollPane, BorderLayout.CENTER);
+ centerPanel.add(loadingPanel, BorderLayout.SOUTH);
+
+ // Add components
+ masterPanel.add(centerPanel, BorderLayout.CENTER);
+ masterPanel.add(inputPanel, BorderLayout.SOUTH);
+
+
+ // Initialize with empty chat container
+ updateOutputArea();
+ }
+
+ private String getCodeStyle() {
+ boolean isDarkTheme = !JBColor.isBright();
+ String backgroundColor = isDarkTheme ? "#2b2d30" : "#fafafa";
+ String textColor = isDarkTheme ? "#bababa" : "#2b2b2b";
+ String codeBackground = isDarkTheme ? "#1e1f22" : "#f6f8fa";
+ String codeBorder = isDarkTheme ? "#1e1f22" : "#e1e4e8";
+ String linkColor = isDarkTheme ? "#589df6" : "#2470B3";
+ String separatorColor = isDarkTheme ? "#3c3f41" : "#e0e0e0";
+
+ return "body { " +
+ "font-family: Arial, sans-serif; " +
+ "font-size: 13pt; " +
+ "margin: 10px; " +
+ "background-color: " + backgroundColor + "; " +
+ "color: " + textColor + "; " +
+ "}\n" +
+ ".message { " +
+ "padding: 12px; " +
+ "margin: 5px 0; " +
+ "}\n" +
+ ".message.user { " +
+ "background-color: " + (isDarkTheme ? "#2d2d2d" : "#e3f2fd") + "; " +
+ "margin-left: 20%; " +
+ "}\n" +
+ ".message.assistant { " +
+ "background-color: " + (isDarkTheme ? "#1e1e1e" : "#f5f5f5") + "; " +
+ "margin-right: 20%; " +
+ "}\n" +
+ ".message.system { " +
+ "background-color: " + (isDarkTheme ? "#2d2d2d" : "#e8f5e9") + "; " +
+ "text-align: center; " +
+ "font-style: italic; " +
+ "}\n" +
+ ".message-header { " +
+ "font-weight: bold; " +
+ "margin-bottom: 4px; " +
+ "}\n" +
+ ".message-content { " +
+ "white-space: pre-wrap; " +
+ "}\n" +
+ ".message-separator { " +
+ "border-top: 1px solid " + separatorColor + "; " +
+ "margin: 10px 0; " +
+ "}\n" +
+ "pre { " +
+ "background-color: " + codeBackground + "; " +
+ "padding: 16px; " +
+ "margin: 1em 0; " +
+ "border: 1px solid " + codeBorder + "; " +
+ "}\n" +
+ "code { " +
+ "font-family: monospace; " +
+ "font-size: 12pt; " +
+ "}\n" +
+ "a { color: " + linkColor + "; }";
+ }
+
+ private String repeatString(String str, int count) {
+ StringBuilder sb = new StringBuilder();
+ for (int i = 0; i < count; i++) {
+ sb.append(str);
+ }
+ return sb.toString();
+ }
+
+ private void startLoading() {
+ isLoading = true;
+ loadingPanel.setVisible(true);
+
+ // Start the loading animation
+ loadingTimer.scheduleAtFixedRate(new TimerTask() {
+ private int dots = 0;
+ @Override
+ public void run() {
+ if (!isLoading) {
+ cancel();
+ return;
+ }
+ SwingUtilities.invokeLater(() -> {
+ dots = (dots + 1) % 4;
+ loadingLabel.setText(I18nService.getInstance().message("llm.thinking") + repeatString(".", dots));
+ });
+ }
+ }, 0, 500);
+
+ }
+
+ private void stopLoading() {
+ if (!isLoading) {
+ return; // Prevent multiple stop calls
+ }
+ isLoading = false;
+ loadingPanel.setVisible(false);
+ loadingTimer.purge();
+ }
+
+ private void updateOutputArea() {
+ String fullHtml = String.format(BASE_HTML_TEMPLATE, getCodeStyle(), chatHistory.toString());
+ SwingUtilities.invokeLater(() -> {
+ try {
+ // Create a new document each time to avoid state issues
+ HTMLDocument doc = (HTMLDocument) htmlKit.createDefaultDocument();
+ // Set the document first
+ outputArea.setDocument(doc);
+ // Then insert the content
+ htmlKit.insertHTML(doc, 0, fullHtml, 0, 0, null);
+ outputArea.setCaretPosition(doc.getLength());
+ } catch (Exception e) {
+ // Fallback to setText if something goes wrong
+ outputArea.setText(fullHtml);
+ }
+ });
+ }
+
+ private void appendMarkdownToOutput(String markdown) {
+ String htmlContent = convertMarkdownToHtml(markdown);
+ chatHistory.append(htmlContent);
+ updateOutputArea();
+ }
+
+ private String convertMarkdownToHtml(String markdown) {
+ Parser parser = Parser.builder().build();
+ Node document = parser.parse(markdown);
+ HtmlRenderer renderer = HtmlRenderer.builder().build();
+ return renderer.render(document);
+ }
+
+ private void clearOutput() {
+ chatHistory.setLength(0);
+ updateOutputArea();
+ }
+
+ private void copyToClipboard(Object eventObj) {
+ String currentContentToCopy;
+
+ if (copyAsMarkdown) {
+ currentContentToCopy = eventObj.toString();
+ } else {
+ currentContentToCopy = outputArea.getText();
+ }
+ StringSelection selection = new StringSelection(currentContentToCopy);
+ Toolkit.getDefaultToolkit().getSystemClipboard().setContents(selection, selection);
+ }
+}
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/components/LLMSuggestionUIComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/LLMSuggestionUIComponent.java
similarity index 63%
rename from src/main/java/com/github/jaksonlin/pitestintellij/components/LLMSuggestionUIComponent.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/LLMSuggestionUIComponent.java
index 9f97bb81..061209d6 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/components/LLMSuggestionUIComponent.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/llmchat/LLMSuggestionUIComponent.java
@@ -1,97 +1,82 @@
-package com.github.jaksonlin.pitestintellij.components;
-
-import com.github.jaksonlin.pitestintellij.context.PitestContext;
-import com.github.jaksonlin.pitestintellij.observers.BasicEventObserver;
-import com.github.jaksonlin.pitestintellij.viewmodels.LLMSuggestionUIComponentViewModel;
+package com.github.jaksonlin.testcraft.presentation.components.llmchat;
+
+import com.github.jaksonlin.testcraft.domain.context.PitestContext;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.RunHistoryEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.ChatEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.TypedEventObserver;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
+import com.github.jaksonlin.testcraft.presentation.viewmodels.LLMSuggestionUIComponentViewModel;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.ui.ComboBox;
import com.intellij.util.ui.JBUI;
-import com.intellij.AbstractBundle;
-import org.jetbrains.annotations.PropertyKey;
-import javax.swing.*;
+import javax.swing.JPanel;
+import javax.swing.JButton;
+import javax.swing.JLabel;
+import javax.swing.JTextField;
+import javax.swing.JComboBox;
import javax.swing.plaf.basic.BasicComboBoxEditor;
-import java.awt.*;
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.ComboBoxEditor;
+import java.awt.BorderLayout;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.FlowLayout;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
-import java.util.ResourceBundle;
-import com.github.jaksonlin.pitestintellij.services.LLMService;
-public class LLMSuggestionUIComponent implements BasicEventObserver {
- private static final String BUNDLE = "messages.MyBundle";
- private static ResourceBundle ourBundle;
-
- public static String message(@PropertyKey(resourceBundle = BUNDLE) String key, Object... params) {
- return AbstractBundle.message(getBundle(), key, params);
- }
-
- private static ResourceBundle getBundle() {
- if (ourBundle == null) {
- ourBundle = ResourceBundle.getBundle(BUNDLE);
+public class LLMSuggestionUIComponent {
+
+ private final TypedEventObserver chatEventObserver = new TypedEventObserver(ChatEvent.class) {
+ @Override
+ public void onTypedEvent(ChatEvent event) {
+ switch (event.getEventType()) {
+ case ChatEvent.START_LOADING:
+ generateButton.setEnabled(false);
+ dryRunButton.setEnabled(false);
+ break;
+ case ChatEvent.STOP_LOADING:
+ case ChatEvent.CHAT_RESPONSE:
+ case ChatEvent.ERROR:
+ generateButton.setEnabled(true);
+ dryRunButton.setEnabled(true);
+ break;
+ }
}
- return ourBundle;
- }
+ };
+ private final TypedEventObserver runHistoryEventObserver = new TypedEventObserver(RunHistoryEvent.class) {
+ @Override
+ public void onTypedEvent(RunHistoryEvent event) {
+ switch (event.getEventType()) {
+ case RunHistoryEvent.RUN_HISTORY_LIST:
+ ApplicationManager.getApplication().invokeLater(() -> loadFileHistory(event.getPayload()));
+ break;
+ }
+ }
+ };;
+
- private final LLMSuggestionUIComponentViewModel viewModel;
- private final ChatPanel chatPanel = new ChatPanel();
- private final LLMResponsePanel responsePanel = new LLMResponsePanel(chatPanel);
+ private final ChatPanelComponent chatPanel = new ChatPanelComponent();
+ private final LLMResponseComponent responsePanel = new LLMResponseComponent(chatPanel);
+
+ private final LLMSuggestionUIComponentViewModel viewModel = new LLMSuggestionUIComponentViewModel();
+
private final JPanel mainPanel = new JPanel(new BorderLayout());
private final DefaultComboBoxModel fileListModel = new DefaultComboBoxModel<>();
private final JComboBox fileSelector = new ComboBox<>(fileListModel);
- private final JButton generateButton = new JButton(message("llm.generate.suggestions"));
- private final JButton dryRunButton = new JButton(message("llm.check.prompt"));
+ private final JButton generateButton = new JButton(I18nService.getInstance().message("llm.generate.suggestions"));
+ private final JButton dryRunButton = new JButton(I18nService.getInstance().message("llm.check.prompt"));
private List allFileItems = new ArrayList<>();
- public LLMSuggestionUIComponent(LLMService llmService) {
+ public LLMSuggestionUIComponent() {
setupUI();
- // setup message routing
- viewModel = new LLMSuggestionUIComponentViewModel(llmService);
- viewModel.addObserver(this);
- viewModel.addObserver(responsePanel);
-
- // add the chatPanel to the mainPanel
- chatPanel.addListener(message -> {
- viewModel.handleChatMessage(message);
- });
-
- // add the reponse listener to the responsePanel
- responsePanel.addResponseActionListener(new LLMResponsePanel.ResponseActionListener() {
- @Override
- public void onClearButtonClick() {
- viewModel.clearChat();
- }
-
- @Override
- public void onCopyButtonClick() {
- viewModel.copyChat();
- }
- });
-
+ // propagate the config change
viewModel.propagateConfigChange();
-
}
-
- @Override
- public void onEventHappen(String eventName, Object eventObj) {
- switch (eventName) {
- case "START_LOADING":
- generateButton.setEnabled(false);
- dryRunButton.setEnabled(false);
- break;
- case "STOP_LOADING":
- generateButton.setEnabled(true);
- dryRunButton.setEnabled(true);
- break;
- case "RUN_HISTORY_LIST":
- ApplicationManager.getApplication().invokeLater(() -> loadFileHistory(eventObj));
- break;
- default:
- break;
- }
- }
public JPanel getPanel() {
return this.mainPanel;
@@ -145,14 +130,14 @@ public Component getEditorComponent() {
// Add components to top panel
JPanel selectorPanel = new JPanel(new BorderLayout());
- selectorPanel.add(new JLabel(message("llm.select.file") + ": "), BorderLayout.WEST);
+ selectorPanel.add(new JLabel(I18nService.getInstance().message("llm.select.file") + ": "), BorderLayout.WEST);
selectorPanel.add(fileSelector, BorderLayout.CENTER);
topPanel.add(selectorPanel, BorderLayout.CENTER);
topPanel.add(buttonPanel, BorderLayout.EAST);
// Add panels to main panel
mainPanel.add(topPanel, BorderLayout.NORTH);
- mainPanel.add(responsePanel, BorderLayout.CENTER);
+ mainPanel.add(responsePanel.getMasterPanel(), BorderLayout.CENTER);
}
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/components/MutationToolWindowUIComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/MutationToolWindowUIComponent.java
similarity index 87%
rename from src/main/java/com/github/jaksonlin/pitestintellij/components/MutationToolWindowUIComponent.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/MutationToolWindowUIComponent.java
index 38bd9818..dddfc08c 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/components/MutationToolWindowUIComponent.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/MutationToolWindowUIComponent.java
@@ -1,7 +1,7 @@
-package com.github.jaksonlin.pitestintellij.components;
+package com.github.jaksonlin.testcraft.presentation.components.mutation;
-import com.github.jaksonlin.pitestintellij.MyBundle;
-import com.github.jaksonlin.pitestintellij.viewmodels.MutationToolWindowViewModel;
+
+import com.github.jaksonlin.testcraft.presentation.viewmodels.MutationToolWindowViewModel;
import com.intellij.openapi.project.Project;
import javax.swing.*;
@@ -13,8 +13,10 @@
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
+
public class MutationToolWindowUIComponent {
- private final JButton clearButton = new JButton(MyBundle.message("clear.button"));
+ private final JButton clearButton = new JButton(I18nService.getInstance().message("clear.button"));
private final JTextField searchInput = new JTextField(20);
protected final ObservableTree resultTree = new ObservableTree();
private final MutationToolWindowViewModel vm;
@@ -24,7 +26,7 @@ public MutationToolWindowUIComponent(Project project) {
this.vm = new MutationToolWindowViewModel(project, resultTree);
this.toolWindowPanel = createToolWindowPanel();
registerListeners();
- searchInput.setToolTipText(MyBundle.message("search.placeholder"));
+ searchInput.setToolTipText(I18nService.getInstance().message("search.placeholder"));
}
private void registerListeners() {
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/components/ObservableTree.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/ObservableTree.java
similarity index 65%
rename from src/main/java/com/github/jaksonlin/pitestintellij/components/ObservableTree.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/ObservableTree.java
index 08548811..9c6af23d 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/components/ObservableTree.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/ObservableTree.java
@@ -1,8 +1,9 @@
-package com.github.jaksonlin.pitestintellij.components;
+package com.github.jaksonlin.testcraft.presentation.components.mutation;
-import com.github.jaksonlin.pitestintellij.MyBundle;
-import com.github.jaksonlin.pitestintellij.observers.BasicEventObserver;
-import com.github.jaksonlin.pitestintellij.util.Pair;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.RunHistoryEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.TypedEventObserver;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
+import com.github.jaksonlin.testcraft.util.Pair;
import org.jetbrains.annotations.NotNull;
import javax.swing.*;
@@ -13,30 +14,34 @@
import java.util.List;
import java.util.Objects;
-public class ObservableTree extends JTree implements BasicEventObserver {
+public class ObservableTree extends JTree {
- @Override
- public void onEventHappen(String eventName,Object eventObj) {
- if (!eventName.equals("RUN_HISTORY")) {
- return;
- }
- if (eventObj == null) {
- initializeMutationTree(Collections.emptyList());
- } else if (eventObj instanceof Pair) {
- Pair, ?> pair = (Pair, ?>) eventObj;
- if (pair.getFirst() instanceof String && pair.getSecond() instanceof String) {
- updateMutationTree(new Pair<>((String) pair.getFirst(), (String) pair.getSecond()));
+ private final TypedEventObserver eventObserver = new TypedEventObserver(RunHistoryEvent.class) {
+ @Override
+ public void onTypedEvent(RunHistoryEvent event) {
+ if (!event.getEventType().equals(RunHistoryEvent.RUN_HISTORY)) {
+ return;
}
- } else if (eventObj instanceof List) {
- List> list = (List>) eventObj;
- if (list.isEmpty()) {
+ if (event.getPayload() == null) {
initializeMutationTree(Collections.emptyList());
- } else if (list.get(0) instanceof Pair) {
- List> nodeList = (List>) list;
- initializeMutationTree(nodeList);
+ } else if (event.getPayload() instanceof Pair) {
+ Pair, ?> pair = (Pair, ?>) event.getPayload();
+ if (pair.getFirst() instanceof String && pair.getSecond() instanceof String) {
+ updateMutationTree(new Pair<>((String) pair.getFirst(), (String) pair.getSecond()));
+ }
+ } else if (event.getPayload() instanceof List) {
+ List> list = (List>) event.getPayload();
+ if (list.isEmpty()) {
+ initializeMutationTree(Collections.emptyList());
+ } else if (list.get(0) instanceof Pair) {
+ List> nodeList = (List>) list;
+ initializeMutationTree(nodeList);
+ }
}
}
- }
+ };
+
+
private void initializeMutationTree(@NotNull List> nodeNameList) {
DefaultTreeModel treeModel = buildTreeModel(nodeNameList);
@@ -44,7 +49,7 @@ private void initializeMutationTree(@NotNull List> nodeName
}
private DefaultTreeModel buildTreeModel(@NotNull List> nodeNameList) {
- DefaultMutableTreeNode root = new DefaultMutableTreeNode(MyBundle.message("mutation.tree.root"));
+ DefaultMutableTreeNode root = new DefaultMutableTreeNode(I18nService.getInstance().message("mutation.tree.root"));
for (Pair pair : nodeNameList) {
String packageName = pair.getFirst();
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/components/PitestOutputDialog.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/PitestOutputDialog.java
similarity index 87%
rename from src/main/java/com/github/jaksonlin/pitestintellij/components/PitestOutputDialog.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/PitestOutputDialog.java
index aa696e95..cc4b14a4 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/components/PitestOutputDialog.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/mutation/PitestOutputDialog.java
@@ -1,5 +1,6 @@
-package com.github.jaksonlin.pitestintellij.components;
+package com.github.jaksonlin.testcraft.presentation.components.mutation;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.I18nService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
@@ -38,7 +39,7 @@ protected JComponent createCenterPanel() {
panel.add(scrollPane, BorderLayout.CENTER);
if (reportFile != null) {
- JButton viewReportButton = new JButton("HTML Report");
+ JButton viewReportButton = new JButton(I18nService.getInstance().message("pitest.view.report"));
viewReportButton.addActionListener(e -> {
try {
Desktop.getDesktop().browse(reportFile.toURI());
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/components/PremiumSettingsComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/premium/PremiumSettingsComponent.java
similarity index 84%
rename from src/main/java/com/github/jaksonlin/pitestintellij/components/PremiumSettingsComponent.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/components/premium/PremiumSettingsComponent.java
index 1369504b..6fe8e2b4 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/components/PremiumSettingsComponent.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/premium/PremiumSettingsComponent.java
@@ -1,6 +1,6 @@
-package com.github.jaksonlin.pitestintellij.components;
+package com.github.jaksonlin.testcraft.presentation.components.premium;
-import com.github.jaksonlin.pitestintellij.license.PremiumManager;
+import com.github.jaksonlin.testcraft.infrastructure.license.PremiumManager;
import javax.swing.*;
diff --git a/src/main/java/com/github/jaksonlin/testcraft/presentation/components/testannotation/AnnotationClassSelectionComponent.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/testannotation/AnnotationClassSelectionComponent.java
new file mode 100644
index 00000000..fb6da0c3
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/components/testannotation/AnnotationClassSelectionComponent.java
@@ -0,0 +1,4 @@
+package com.github.jaksonlin.testcraft.presentation.components.testannotation;
+
+public class AnnotationClassSelectionComponent {
+}
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/toolWindow/LLMSuggestionsToolWindowFactory.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/toolWindow/LLMSuggestionsToolWindowFactory.java
similarity index 64%
rename from src/main/java/com/github/jaksonlin/pitestintellij/toolWindow/LLMSuggestionsToolWindowFactory.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/toolWindow/LLMSuggestionsToolWindowFactory.java
index 36d6a437..7a8d0d62 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/toolWindow/LLMSuggestionsToolWindowFactory.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/toolWindow/LLMSuggestionsToolWindowFactory.java
@@ -1,9 +1,7 @@
-package com.github.jaksonlin.pitestintellij.toolWindow;
+package com.github.jaksonlin.testcraft.presentation.toolWindow;
-import com.github.jaksonlin.pitestintellij.components.LLMSuggestionUIComponent;
-import com.github.jaksonlin.pitestintellij.services.LLMService;
-import com.github.jaksonlin.pitestintellij.services.RunHistoryManagerService;
-import com.intellij.openapi.application.ApplicationManager;
+import com.github.jaksonlin.testcraft.presentation.components.llmchat.LLMSuggestionUIComponent;
+import com.github.jaksonlin.testcraft.infrastructure.services.business.RunHistoryManagerService;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
@@ -18,16 +16,14 @@ public class LLMSuggestionsToolWindowFactory implements ToolWindowFactory {
@Override
public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
// create a new LLMSuggestionUIComponent
- LLMService llmService = ApplicationManager.getApplication().getService(LLMService.class);
- LLMSuggestionUIComponent uiComponent = new LLMSuggestionUIComponent(llmService);
+ LLMSuggestionUIComponent uiComponent = new LLMSuggestionUIComponent();
// add the uiComponent to the toolWindow through the content manager
JPanel toolWindowPanel = uiComponent.getPanel();
ContentManager contentManager = toolWindow.getContentManager();
Content content = new ContentImpl(toolWindowPanel, "TestCraft - LLM Suggestions Tool Window", false); // Directly create ContentImpl
contentManager.addContent(content);
// register the uiComponent to the runHistoryManagerService to sync the run history
- RunHistoryManagerService runHistoryManagerService = project.getService(RunHistoryManagerService.class);
- runHistoryManagerService.addObserver(uiComponent);
+ RunHistoryManagerService.getInstance().addObserver(uiComponent);
}
}
\ No newline at end of file
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/toolWindow/MutationToolWindowFactory.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/toolWindow/MutationToolWindowFactory.java
similarity index 85%
rename from src/main/java/com/github/jaksonlin/pitestintellij/toolWindow/MutationToolWindowFactory.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/toolWindow/MutationToolWindowFactory.java
index 6565f88a..3f45fe69 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/toolWindow/MutationToolWindowFactory.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/toolWindow/MutationToolWindowFactory.java
@@ -1,6 +1,6 @@
-package com.github.jaksonlin.pitestintellij.toolWindow;
+package com.github.jaksonlin.testcraft.presentation.toolWindow;
-import com.github.jaksonlin.pitestintellij.components.MutationToolWindowUIComponent;
+import com.github.jaksonlin.testcraft.presentation.components.mutation.MutationToolWindowUIComponent;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.wm.ToolWindow;
import com.intellij.openapi.wm.ToolWindowFactory;
diff --git a/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/LLMSuggestionUIComponentViewModel.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/LLMSuggestionUIComponentViewModel.java
new file mode 100644
index 00000000..52e95e5b
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/LLMSuggestionUIComponentViewModel.java
@@ -0,0 +1,52 @@
+package com.github.jaksonlin.testcraft.presentation.viewmodels;
+
+import com.github.jaksonlin.testcraft.domain.context.PitestContext;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.ChatEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.TypedEventObserver;
+import com.github.jaksonlin.testcraft.infrastructure.services.config.LLMConfigService;
+import com.github.jaksonlin.testcraft.infrastructure.services.system.EventBusService;
+
+
+public class LLMSuggestionUIComponentViewModel {
+ private final LLMConfigService llmConfigService = LLMConfigService.getInstance();
+ private final EventBusService eventBusService = EventBusService.getInstance();
+
+ private final TypedEventObserver chatObserver = new TypedEventObserver(ChatEvent.class) {
+ @Override
+ public void onTypedEvent(ChatEvent event) {
+ switch (event.getEventType()) {
+ case ChatEvent.REQUEST_COPY_CHAT_RESPONSE:
+ String chatHistory = llmConfigService.getChatHistory();
+ // when chatHistory is empty, use the lastDryRunPrompt
+ if (chatHistory.isEmpty()) {
+ chatHistory = lastDryRunPrompt;
+ }
+ eventBusService.post(new ChatEvent(ChatEvent.COPY_CHAT_RESPONSE, chatHistory));
+ break;
+ default:
+ break;
+ }
+ }
+ };
+
+ public void propagateConfigChange() {
+ llmConfigService.propagateConfigChange();
+ }
+
+
+ public void generateSuggestions(PitestContext context) {
+ eventBusService.post(new ChatEvent(ChatEvent.START_LOADING, null));
+ llmConfigService.generateUnittestRequest(context.getTestFilePath(), context.getTargetClassFilePath(), context.getMutationResults());
+ }
+
+ private String lastDryRunPrompt = "";
+
+ public void dryRunGetPrompt(PitestContext context) {
+ lastDryRunPrompt = llmConfigService.dryRunGetPrompt(context.getFullyQualifiedTargetTestClassName(), context.getTargetClassFullyQualifiedName(), context.getMutationResults());
+ eventBusService.post(new ChatEvent(ChatEvent.DRY_RUN_PROMPT, lastDryRunPrompt));
+ }
+
+ public void handleChatMessage(String message) {
+ llmConfigService.handleChatMessage(message);
+ }
+}
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/viewmodels/MutationToolWindowViewModel.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/MutationToolWindowViewModel.java
similarity index 76%
rename from src/main/java/com/github/jaksonlin/pitestintellij/viewmodels/MutationToolWindowViewModel.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/MutationToolWindowViewModel.java
index 14fb11c0..7df834e1 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/viewmodels/MutationToolWindowViewModel.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/MutationToolWindowViewModel.java
@@ -1,9 +1,9 @@
-package com.github.jaksonlin.pitestintellij.viewmodels;
+package com.github.jaksonlin.testcraft.presentation.viewmodels;
-import com.github.jaksonlin.pitestintellij.mediators.IMutationMediator;
-import com.github.jaksonlin.pitestintellij.mediators.MutationMediatorImpl;
-import com.github.jaksonlin.pitestintellij.services.RunHistoryManagerService;
-import com.github.jaksonlin.pitestintellij.components.ObservableTree;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.mediators.IMutationMediator;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.mediators.MutationMediatorImpl;
+import com.github.jaksonlin.testcraft.infrastructure.services.business.RunHistoryManagerService;
+import com.github.jaksonlin.testcraft.presentation.components.mutation.ObservableTree;
import com.intellij.openapi.project.Project;
import org.jetbrains.annotations.Nullable;
@@ -12,12 +12,11 @@
import java.util.Enumeration;
public class MutationToolWindowViewModel {
- private final RunHistoryManagerService runHistoryManager;
+ private final RunHistoryManagerService runHistoryManager = RunHistoryManagerService.getInstance();
private final IMutationMediator mutationReportMediator = new MutationMediatorImpl();
private final MutationTreeMediatorViewModel mutationTreeMediatorVM;
public MutationToolWindowViewModel(Project project, ObservableTree mutationTree) {
- this.runHistoryManager = project.getService(RunHistoryManagerService.class);
this.mutationTreeMediatorVM = new MutationTreeMediatorViewModel(project, mutationReportMediator);
runHistoryManager.addObserver(mutationTree);
}
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.java b/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/MutationTreeMediatorViewModel.java
similarity index 86%
rename from src/main/java/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.java
rename to src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/MutationTreeMediatorViewModel.java
index 685d2dae..a2bbe574 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/viewmodels/MutationTreeMediatorViewModel.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/presentation/viewmodels/MutationTreeMediatorViewModel.java
@@ -1,11 +1,10 @@
-package com.github.jaksonlin.pitestintellij.viewmodels;
-
-import com.github.jaksonlin.pitestintellij.context.PitestContext;
-import com.github.jaksonlin.pitestintellij.mediators.IMutationMediator;
-import com.github.jaksonlin.pitestintellij.mediators.IMutationReportUI;
-import com.github.jaksonlin.pitestintellij.services.RunHistoryManagerService;
-import com.github.jaksonlin.pitestintellij.util.Mutation;
-import com.github.jaksonlin.pitestintellij.util.Pair;
+package com.github.jaksonlin.testcraft.presentation.viewmodels;
+
+import com.github.jaksonlin.testcraft.domain.context.PitestContext;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.mediators.IMutationMediator;
+import com.github.jaksonlin.testcraft.infrastructure.services.business.RunHistoryManagerService;
+import com.github.jaksonlin.testcraft.util.Mutation;
+import com.github.jaksonlin.testcraft.util.Pair;
import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
@@ -29,24 +28,33 @@
import javax.swing.*;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreePath;
-import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.MutationEvent;
+import com.github.jaksonlin.testcraft.infrastructure.messaging.events.TypedEventObserver;
-public class MutationTreeMediatorViewModel implements IMutationReportUI {
+public class MutationTreeMediatorViewModel {
private static final Logger log = LoggerFactory.getLogger(MutationTreeMediatorViewModel.class);
private final Project project;
private final IMutationMediator mediator;
- private final RunHistoryManagerService runHistoryManager;
+ private final RunHistoryManagerService runHistoryManager = RunHistoryManagerService.getInstance();
protected final HashMap annotatedNodes = new HashMap<>();
+ private final TypedEventObserver mutationObserver = new TypedEventObserver(MutationEvent.class) {
+ @Override
+ public void onTypedEvent(MutationEvent event) {
+ if (event.getEventType().equals(MutationEvent.MUTATION_RESULT)) {
+ Pair>> payload = (Pair>>) event.getPayload();
+ SwingUtilities.invokeLater(() -> updateMutationResult(payload.getFirst(), payload.getSecond()));
+ }
+ }
+ };
+
public MutationTreeMediatorViewModel(@NotNull Project project, @NotNull IMutationMediator mediator) {
this.project = project;
this.mediator = mediator;
- this.runHistoryManager = project.getService(RunHistoryManagerService.class);
- mediator.register(this);
registerEditorListener(project);
}
@@ -63,7 +71,6 @@ public void fileClosed(@NotNull FileEditorManager source, @NotNull VirtualFile f
});
}
- @Override
public void updateMutationResult(String mutationClassFilePath, Map> mutationTestResult) {
VirtualFile virtualFile = LocalFileSystem.getInstance().findFileByPath(mutationClassFilePath);
if (virtualFile != null) {
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/Blocks.java b/src/main/java/com/github/jaksonlin/testcraft/util/Blocks.java
similarity index 90%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/Blocks.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/Blocks.java
index a81dcfa2..5d9ecdd3 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/Blocks.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/Blocks.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/ClassFileInfo.java b/src/main/java/com/github/jaksonlin/testcraft/util/ClassFileInfo.java
similarity index 92%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/ClassFileInfo.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/ClassFileInfo.java
index 30be685e..80bf9d0c 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/ClassFileInfo.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/ClassFileInfo.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
public class ClassFileInfo {
private final String fullyQualifiedName;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/FileUtils.java b/src/main/java/com/github/jaksonlin/testcraft/util/FileUtils.java
similarity index 97%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/FileUtils.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/FileUtils.java
index 63cba221..244efd64 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/FileUtils.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/FileUtils.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import java.io.File;
import java.io.IOException;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/GitCommitInfo.java b/src/main/java/com/github/jaksonlin/testcraft/util/GitCommitInfo.java
similarity index 92%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/GitCommitInfo.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/GitCommitInfo.java
index 8535f30e..46df17d5 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/GitCommitInfo.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/GitCommitInfo.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
public class GitCommitInfo {
private final String author;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/GitUserInfo.java b/src/main/java/com/github/jaksonlin/testcraft/util/GitUserInfo.java
similarity index 92%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/GitUserInfo.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/GitUserInfo.java
index 92b2a50d..5ce4ec44 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/GitUserInfo.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/GitUserInfo.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
public class GitUserInfo {
private final String name;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/GitUtil.java b/src/main/java/com/github/jaksonlin/testcraft/util/GitUtil.java
similarity index 52%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/GitUtil.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/GitUtil.java
index 7fea8114..6dd7c0e9 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/GitUtil.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/GitUtil.java
@@ -1,6 +1,7 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.intellij.openapi.application.ApplicationManager;
+import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
@@ -15,11 +16,44 @@
import git4idea.repo.GitRepository;
import git4idea.repo.GitRepositoryManager;
+import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeoutException;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.Executors;
public class GitUtil {
+ private static final Logger logger = Logger.getInstance(GitUtil.class);
+ private static final ThreadPoolExecutor gitThreadPool = new ThreadPoolExecutor(
+ 4, // core pool size
+ 8, // max pool size
+ 10L, // reduced keep alive time to release threads faster
+ TimeUnit.SECONDS,
+ new LinkedBlockingQueue<>(50),
+ new ThreadFactory() {
+ private final AtomicInteger counter = new AtomicInteger(1);
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread thread = new Thread(r, "GitCommandThread-" + counter.getAndIncrement());
+ thread.setDaemon(true);
+ return thread;
+ }
+ },
+ new ThreadPoolExecutor.AbortPolicy()
+ ) {
+ @Override
+ protected void afterExecute(Runnable r, Throwable t) {
+ super.afterExecute(r, t);
+ purge();
+ allowCoreThreadTimeOut(true);
+ }
+ };
+ private static final long GIT_COMMAND_TIMEOUT = 30; // seconds
public static GitRepositoryManager getRepositoryManager(Project project) {
return GitRepositoryManager.getInstance(project);
@@ -35,30 +69,63 @@ public static GitRepository getRepositoryForFile(Project project, VirtualFile fi
return GitRepositoryManager.getInstance(project).getRepositoryForFile(file);
}
- public static GitUserInfo getGitUserInfo(Project project) {
- GitRepositoryManager repositoryManager = getRepositoryManager(project);
- List repositories = repositoryManager.getRepositories();
- GitRepository repository = repositories.isEmpty() ? null : repositories.get(0);
-
- if (repository != null) {
+ private static CompletableFuture> runGitCommandAsync(GitLineHandler handler) {
+ Git git = Git.getInstance();
+ boolean inDispatchThread = ApplicationManager.getApplication().isDispatchThread();
+
+ Callable> runCommandCallable = () -> {
try {
- Callable getNameCallable = () -> GitConfigUtil.getValue(project, repository.getRoot(), GitConfigUtil.USER_NAME);
- String name = ApplicationManager.getApplication().executeOnPooledThread(getNameCallable).get();
+ logger.info("Starting git command execution in thread: " + Thread.currentThread().getName());
+ List result = git.runCommand(handler).getOutput();
+ logger.info("Completed git command execution in thread: " + Thread.currentThread().getName());
+ return result;
+ } catch (Exception e) {
+ logger.error("Error executing git command in thread " + Thread.currentThread().getName(), e);
+ throw e;
+ } finally {
+ gitThreadPool.purge();
+ }
+ };
- Callable getEmailCallable = () -> GitConfigUtil.getValue(project, repository.getRoot(), GitConfigUtil.USER_EMAIL);
- String email = ApplicationManager.getApplication().executeOnPooledThread(getEmailCallable).get();
+ CompletableFuture> future = new CompletableFuture<>();
+
+ try {
+ logger.info("Submitting git command to thread pool. Current active threads: " +
+ gitThreadPool.getActiveCount() + ", Queue size: " + gitThreadPool.getQueue().size());
+
+ gitThreadPool.submit(() -> {
+ try {
+ List result = runCommandCallable.call();
+ future.complete(result);
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+ });
+
+ // Set a timeout
+ long timeout = inDispatchThread ? 5 : GIT_COMMAND_TIMEOUT;
+ ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
+ scheduler.schedule(() -> {
+ if (!future.isDone()) {
+ future.completeExceptionally(new TimeoutException("Git command timed out after " + timeout + " seconds"));
+ }
+ scheduler.shutdown();
+ }, timeout, TimeUnit.SECONDS);
+
+ } catch (Exception e) {
+ future.completeExceptionally(e);
+ }
+
+ return future;
+ }
- return new GitUserInfo(
- name != null ? name : "Unknown",
- email != null ? email : "unknown@email.com"
- );
- } catch (InterruptedException | ExecutionException e) {
- // Handle exception appropriately, maybe log it
- e.printStackTrace();
- return new GitUserInfo("Unknown", "unknown@email.com");
- }
+ private static List runGitCommand(GitLineHandler handler) {
+ try {
+ return runGitCommandAsync(handler).get();
+ } catch (InterruptedException | ExecutionException e) {
+ logger.error("Failed to execute git command", e);
+ throw new RuntimeException("Git command execution failed: " + e.getMessage(), e);
}
- return new GitUserInfo("Unknown", "unknown@email.com");
}
public static GitCommitInfo getLastCommitInfo(Project project, PsiFile file) {
@@ -67,20 +134,24 @@ public static GitCommitInfo getLastCommitInfo(Project project, PsiFile file) {
if (repository == null) {
return null;
}
- Git git = Git.getInstance();
GitLineHandler handler = new GitLineHandler(project, repository.getRoot(), GitCommand.LOG);
- handler.addParameters(
- "--max-count=1",
- "--pretty=format:%an|%ae|%ad|%s",
- "--date=format:%Y-%m-%d %H:%M:%S",
- "--",
- virtualFile.getPath()
- );
+
+ List gitCommandArgs = new ArrayList<>();
+ gitCommandArgs.add("--max-count=1");
+ gitCommandArgs.add("--pretty=format:%an|%ae|%ad|%s");
+ gitCommandArgs.add("--date=format:%Y-%m-%d %H:%M:%S");
+ gitCommandArgs.add("--");
+ gitCommandArgs.add(virtualFile.getPath());
+ handler.addParameters(gitCommandArgs);
+ StringBuilder command = new StringBuilder();
+
+ for (String arg : gitCommandArgs) {
+ command.append(arg).append(" ");
+ }
+ logger.info("Running git command: " + command.toString());
- try {
- Callable> runCommandCallable = () -> git.runCommand(handler).getOutput();
- List output = ApplicationManager.getApplication().executeOnPooledThread(runCommandCallable).get();
+ List output = runGitCommand(handler);
String firstLine = output.isEmpty() ? null : output.get(0);
if (firstLine != null) {
String[] parts = firstLine.split("\\|");
@@ -93,10 +164,7 @@ public static GitCommitInfo getLastCommitInfo(Project project, PsiFile file) {
);
}
}
- } catch (InterruptedException | ExecutionException e) {
- // Handle exception appropriately, maybe log it
- e.printStackTrace();
- }
+
return null;
}
@@ -110,7 +178,6 @@ public static GitUserInfo getFirstCreatorInfo(Project project, PsiMethod psiMeth
if (repository == null) {
return null;
}
- Git git = Git.getInstance();
Document document = FileDocumentManager.getInstance().getDocument(file);
if (document == null) {
@@ -122,15 +189,21 @@ public static GitUserInfo getFirstCreatorInfo(Project project, PsiMethod psiMeth
int endLine = document.getLineNumber(endOffset) + 1;
GitLineHandler handler = new GitLineHandler(project, repository.getRoot(), GitCommand.BLAME);
- handler.addParameters(
- "-L", startLine + "," + endLine,
- "--porcelain",
- file.getPath()
- );
+
+ List gitCommandArgs = new ArrayList<>();
+ gitCommandArgs.add("-L");
+ gitCommandArgs.add(startLine + "," + endLine);
+ gitCommandArgs.add("--porcelain");
+ gitCommandArgs.add(file.getPath());
+ handler.addParameters(gitCommandArgs);
+ StringBuilder command = new StringBuilder();
+ for (String arg : gitCommandArgs) {
+ command.append(arg).append(" ");
+ }
+ logger.info("Running git command: " + command.toString());
- try {
- Callable> runCommandCallable = () -> git.runCommand(handler).getOutput();
- List output = ApplicationManager.getApplication().executeOnPooledThread(runCommandCallable).get();
+
+ List output = runGitCommand(handler);
String authorName = null;
String email = null;
@@ -169,43 +242,55 @@ public static GitUserInfo getFirstCreatorInfo(Project project, PsiMethod psiMeth
timestamp != null ? timestamp : System.currentTimeMillis()
);
}
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
- }
+
return null;
}
public static GitUserInfo getLastModifyInfo(Project project, PsiMethod psiMethod) {
- PsiFile containingFile = psiMethod.getContainingFile();
- VirtualFile file = containingFile != null ? containingFile.getVirtualFile() : null;
- if (file == null) {
- return null;
- }
- GitRepository repository = getRepositoryForFile(project, file);
- if (repository == null) {
- return null;
- }
- Git git = Git.getInstance();
-
- Document document = FileDocumentManager.getInstance().getDocument(file);
- if (document == null) {
- return null;
- }
- int startOffset = psiMethod.getTextRange().getStartOffset();
- int endOffset = psiMethod.getTextRange().getEndOffset();
- int startLine = document.getLineNumber(startOffset) + 1;
- int endLine = document.getLineNumber(endOffset) + 1;
+ try {
+ PsiFile containingFile = psiMethod.getContainingFile();
+ VirtualFile file = containingFile != null ? containingFile.getVirtualFile() : null;
+ if (file == null) {
+ logger.error("Cannot get virtual file for method: " + psiMethod.getName());
+ return getGitUserInfo(project);
+ }
+
+ GitRepository repository = getRepositoryForFile(project, file);
+ if (repository == null) {
+ logger.error("Cannot find git repository for file: " + file.getPath());
+ return getGitUserInfo(project);
+ }
- GitLineHandler handler = new GitLineHandler(project, repository.getRoot(), GitCommand.BLAME);
- handler.addParameters(
- "-L", startLine + "," + endLine,
- "--porcelain",
- file.getPath()
- );
+ Document document = FileDocumentManager.getInstance().getDocument(file);
+ if (document == null) {
+ logger.error("Cannot get document for file: " + file.getPath());
+ return getGitUserInfo(project);
+ }
+
+ int startOffset = psiMethod.getTextRange().getStartOffset();
+ int endOffset = psiMethod.getTextRange().getEndOffset();
+ int startLine = document.getLineNumber(startOffset) + 1;
+ int endLine = document.getLineNumber(endOffset) + 1;
+
+ GitLineHandler handler = new GitLineHandler(project, repository.getRoot(), GitCommand.BLAME);
+
+ List gitCommandArgs = new ArrayList<>();
+ gitCommandArgs.add("-L");
+ gitCommandArgs.add(startLine + "," + endLine);
+ gitCommandArgs.add("--porcelain");
+ gitCommandArgs.add(file.getPath());
+ handler.addParameters(gitCommandArgs);
+ StringBuilder command = new StringBuilder();
+ for (String arg : gitCommandArgs) {
+ command.append(arg).append(" ");
+ }
+ logger.info("Running git command: " + command.toString());
- try {
- Callable> runCommandCallable = () -> git.runCommand(handler).getOutput();
- List output = ApplicationManager.getApplication().executeOnPooledThread(runCommandCallable).get();
+ List output = runGitCommand(handler);
+ if (output == null || output.isEmpty()) {
+ logger.info("No output from git blame command");
+ return getGitUserInfo(project);
+ }
BlameInfo latestCommit = null;
BlameInfo currentCommit = null;
@@ -247,7 +332,7 @@ public static GitUserInfo getLastModifyInfo(Project project, PsiMethod psiMethod
latestCommit = currentCommit;
}
} catch (NumberFormatException e) {
- e.printStackTrace();
+ logger.error("Error parsing timestamp from git blame output", e);
}
currentCommit = null;
}
@@ -260,10 +345,57 @@ public static GitUserInfo getLastModifyInfo(Project project, PsiMethod psiMethod
latestCommit.getTimestamp() != null ? latestCommit.getTimestamp() : System.currentTimeMillis()
);
}
- } catch (InterruptedException | ExecutionException e) {
- e.printStackTrace();
+
+ // If we couldn't get the last modify info, return current user info
+ logger.info("Could not determine last modifier, using current user info");
+ return getGitUserInfo(project);
+
+ } catch (Exception e) {
+ logger.error("Error getting last modify info", e);
+ return getGitUserInfo(project);
+ }
+ }
+
+ public static CompletableFuture getGitUserInfoAsync(Project project) {
+ GitRepositoryManager repositoryManager = getRepositoryManager(project);
+ List repositories = repositoryManager.getRepositories();
+ GitRepository repository = repositories.isEmpty() ? null : repositories.get(0);
+
+ if (repository != null) {
+ CompletableFuture nameFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ return GitConfigUtil.getValue(project, repository.getRoot(), GitConfigUtil.USER_NAME);
+ } catch (Exception e) {
+ logger.error("Error getting git user name", e);
+ return "Unknown";
+ }
+ }, gitThreadPool);
+
+ CompletableFuture emailFuture = CompletableFuture.supplyAsync(() -> {
+ try {
+ return GitConfigUtil.getValue(project, repository.getRoot(), GitConfigUtil.USER_EMAIL);
+ } catch (Exception e) {
+ logger.error("Error getting git user email", e);
+ return "unknown@email.com";
+ }
+ }, gitThreadPool);
+
+ return CompletableFuture.allOf(nameFuture, emailFuture)
+ .thenApply(v -> new GitUserInfo(
+ nameFuture.join() != null ? nameFuture.join() : "Unknown",
+ emailFuture.join() != null ? emailFuture.join() : "unknown@email.com"
+ ));
+ }
+ return CompletableFuture.completedFuture(new GitUserInfo("Unknown", "unknown@email.com"));
+ }
+
+ public static GitUserInfo getGitUserInfo(Project project) {
+ try {
+ return getGitUserInfoAsync(project).get(5, TimeUnit.SECONDS);
+ } catch (Exception e) {
+ logger.error("Failed to get git user info", e);
+ return new GitUserInfo("Unknown", "unknown@email.com");
}
- return null;
}
private static class BlameInfo {
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/GradleUtils.java b/src/main/java/com/github/jaksonlin/testcraft/util/GradleUtils.java
similarity index 98%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/GradleUtils.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/GradleUtils.java
index c274f731..9f671b58 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/GradleUtils.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/GradleUtils.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleManager;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/Indexes.java b/src/main/java/com/github/jaksonlin/testcraft/util/Indexes.java
similarity index 90%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/Indexes.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/Indexes.java
index 74335cf3..6aa11edd 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/Indexes.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/Indexes.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/JavaFileProcessor.java b/src/main/java/com/github/jaksonlin/testcraft/util/JavaFileProcessor.java
similarity index 97%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/JavaFileProcessor.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/JavaFileProcessor.java
index c0846321..8827356c 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/JavaFileProcessor.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/JavaFileProcessor.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseResult;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/Mutation.java b/src/main/java/com/github/jaksonlin/testcraft/util/Mutation.java
similarity index 98%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/Mutation.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/Mutation.java
index f29fabda..0c9e25fd 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/Mutation.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/Mutation.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
public class Mutation {
private boolean detected;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/MutationReportParser.java b/src/main/java/com/github/jaksonlin/testcraft/util/MutationReportParser.java
similarity index 95%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/MutationReportParser.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/MutationReportParser.java
index 46cc2c2f..58ba1ff2 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/MutationReportParser.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/MutationReportParser.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationFeature;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/Mutations.java b/src/main/java/com/github/jaksonlin/testcraft/util/Mutations.java
similarity index 93%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/Mutations.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/Mutations.java
index 2a83f781..6fe3e074 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/Mutations.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/Mutations.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlElementWrapper;
diff --git a/src/main/java/com/github/jaksonlin/testcraft/util/OllamaClient.java b/src/main/java/com/github/jaksonlin/testcraft/util/OllamaClient.java
new file mode 100644
index 00000000..ee49f253
--- /dev/null
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/OllamaClient.java
@@ -0,0 +1,103 @@
+package com.github.jaksonlin.testcraft.util;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.intellij.openapi.diagnostic.Logger;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class OllamaClient {
+ private static final Logger LOG = Logger.getInstance(OllamaClient.class);
+ private final String baseUrl;
+ private final ObjectMapper objectMapper;
+ private final CloseableHttpClient httpClient;
+ private final int timeoutSeconds;
+ private final String model;
+ private final int maxTokens;
+ private final float temperature;
+
+ public OllamaClient(String host, String model, int maxTokens, float temperature, int port, int timeoutSeconds) {
+ this.baseUrl = String.format("http://%s:%d", host, port);
+ this.objectMapper = new ObjectMapper();
+ this.timeoutSeconds = timeoutSeconds;
+ this.model = model;
+ this.maxTokens = maxTokens;
+ this.temperature = temperature;
+
+ RequestConfig config = RequestConfig.custom()
+ .setConnectTimeout(timeoutSeconds * 1000)
+ .setConnectionRequestTimeout(timeoutSeconds * 1000)
+ .setSocketTimeout(timeoutSeconds * 1000)
+ .build();
+
+ this.httpClient = HttpClients.custom()
+ .setDefaultRequestConfig(config)
+ .build();
+ }
+
+ public boolean testConnection() {
+ try {
+ HttpGet request = new HttpGet(baseUrl);
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ return response.getStatusLine().getStatusCode() == 200;
+ }
+ } catch (Exception e) {
+ LOG.warn("Failed to test connection to Ollama server", e);
+ return false;
+ }
+ }
+
+ public String chatCompletion(List messages) throws IOException {
+ Map requestBody = new HashMap<>();
+ requestBody.put("model", model);
+ requestBody.put("messages", messages);
+ requestBody.put("stream", false);
+ requestBody.put("temperature", temperature);
+ requestBody.put("num_predict", maxTokens);
+
+ HttpPost request = new HttpPost(baseUrl + "/api/chat");
+ request.setHeader("Content-Type", "application/json");
+ request.setEntity(new StringEntity(objectMapper.writeValueAsString(requestBody), "UTF-8"));
+
+ try (CloseableHttpResponse response = httpClient.execute(request)) {
+ if (response.getStatusLine().getStatusCode() != 200) {
+ String errorBody = EntityUtils.toString(response.getEntity());
+ LOG.error("Error from Ollama API: " + errorBody);
+ throw new IOException("Failed to get response from Ollama API: " + response.getStatusLine().getStatusCode());
+ }
+
+ String responseBody = EntityUtils.toString(response.getEntity());
+ Map responseMap = objectMapper.readValue(responseBody, Map.class);
+ return ((Map) responseMap.get("message")).get("content");
+ }
+ }
+
+ public static class Message {
+ private String role;
+ private String content;
+
+ public Message(String role, String content) {
+ this.role = role;
+ this.content = content;
+ }
+
+ public String getRole() {
+ return role;
+ }
+
+ public String getContent() {
+ return content;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/Pair.java b/src/main/java/com/github/jaksonlin/testcraft/util/Pair.java
similarity index 88%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/Pair.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/Pair.java
index 30958f3d..cae8258f 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/Pair.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/Pair.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
public class Pair {
private final K key;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/ProcessExecutor.java b/src/main/java/com/github/jaksonlin/testcraft/util/ProcessExecutor.java
similarity index 94%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/ProcessExecutor.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/ProcessExecutor.java
index 571c50e6..d4df33c5 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/ProcessExecutor.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/ProcessExecutor.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import java.io.BufferedReader;
import java.io.IOException;
@@ -8,10 +8,11 @@
import java.util.List;
public class ProcessExecutor {
- public static ProcessResult executeProcess(List command) {
+ public static ProcessResult executeProcess(List command, String workingDirectory) {
try {
ProcessBuilder builder = new ProcessBuilder(command);
builder.environment().put("GRADLE_OPTS", "-Dorg.gradle.daemon=false -Dorg.gradle.debug=true");
+ builder.directory(workingDirectory != null ? new java.io.File(workingDirectory) : null);
Process process = builder.start();
StringBuilder output = new StringBuilder();
StringBuilder errorOutput = new StringBuilder();
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/ProcessResult.java b/src/main/java/com/github/jaksonlin/testcraft/util/ProcessResult.java
similarity index 91%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/ProcessResult.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/ProcessResult.java
index 1bbd4e33..b19f1073 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/ProcessResult.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/ProcessResult.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
public class ProcessResult {
private final int exitCode;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/PsiUtil.java b/src/main/java/com/github/jaksonlin/testcraft/util/PsiUtil.java
similarity index 96%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/PsiUtil.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/PsiUtil.java
index 2b40b819..cc23ecc9 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/PsiUtil.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/PsiUtil.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
diff --git a/src/main/java/com/github/jaksonlin/pitestintellij/util/TargetClassInfo.java b/src/main/java/com/github/jaksonlin/testcraft/util/TargetClassInfo.java
similarity index 88%
rename from src/main/java/com/github/jaksonlin/pitestintellij/util/TargetClassInfo.java
rename to src/main/java/com/github/jaksonlin/testcraft/util/TargetClassInfo.java
index 38c7f377..00e3a62c 100644
--- a/src/main/java/com/github/jaksonlin/pitestintellij/util/TargetClassInfo.java
+++ b/src/main/java/com/github/jaksonlin/testcraft/util/TargetClassInfo.java
@@ -1,4 +1,4 @@
-package com.github.jaksonlin.pitestintellij.util;
+package com.github.jaksonlin.testcraft.util;
import java.nio.file.Path;
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 8f16c3d4..6c80e353 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -1,83 +1,143 @@
- Bundled with PIT 1.16.1
-
- Adds a 'Unittest Helpers' menu to the right-click context menu in the editor, and the `Run Mutation Test` action to the submenu.
-
- Adds a 'Mutation Test History' tool window to display the history of mutation tests for each of the `mutated` classes.
-
- You can navigate to the source code of the mutated class by searching on the class name with enter key.
-
- Adds rendering of the mutation test result on the code editor.
-
- Automatic detection of the classpath dependencies for running the mutation test, making use of the Gradle API from the IntelliJ IDEA.
-
- Support long classpath for running the mutation test, especially for enterprise projects when there are thousands of classes in the classpath; and even jar files from other locations.
-
- Additional classes in resource directory (marked as resource in IDE) are added to the classpath for mutation testing.
-
- Usage: Right-click on a JUnit test file and select `Unittest Helpers` -> `Run Mutation Test` to run PIT mutation testing on the test file.
-
- ]]>
-
- 1.0.1-beta
+ TestCraft Pro is a comprehensive Java testing toolkit that enhances test quality through multiple features:
+
+
+ - Mutation Testing
+
+ - Run PITest on both Gradle and Maven projects
+ - Visual results in dedicated tool window
+ - Source code navigation and decoration
+
+
+ - Test Case Management
+
+ - Automated test case annotation generation
+ - Annotation validation and inspection
+ - Smart code completion for annotations
+
+
+ - Test Quality Assurance
+
+ - Assertion statement validation
+ - Missing assertion detection
+ - Test documentation verification
+
+
+
+
+ Originally evolved from pitest-gradle, TestCraft Pro has expanded to become a complete testing toolkit while maintaining all mutation testing capabilities.
+ ]]>
+
+ Change Notes
+ 1.0.10 - 2025-05-06
+
+ - bugfix, git command execution in a separate thread to avoid UI blocking
+
+ 1.0.9 - 2025-04-30
+
+ - bugfix, backward compatibility to JDK1.8 support to run in older IntelliJ IDEA version
+ - upload to pitest-gradle also, for pitest-gradle user, please uninstall pitest-gradle plugin and reinstall the new one manged in [testcraft](https://plugins.jetbrains.com/plugin/27221-testcraft-pro).
+
+ 1.0.8 - 2025-04-28
+
+ - Run pitest in the test file's source root as the process working directory
+ - Dump pitest context into report directory for issue debug
+ - Enhance output information for issue debug
+
+ 1.0.7 - 2025-04-16
+
+ - enhance the dump prompt to use compact prompt
+ - i18n enhancement
+ - bugfixes for no mutation can provide suggestions
+
+ 1.0.6 - 2025-04-09
+
+ - Add ollama access to evaulate the unit test and create new test
+
+
+ 1.0.5 - 2025-04-01
+
+ - Added assertion statement validation
+ - Enhanced test quality checks
+ - Added compatibility with IDE version 251
+
+
+ 1.0.4 - 2025-01-16
+
+ - Added compatibility to IDE version 251
+
+
+ 1.0.3 - 2024-11-14
+
+ - Added unit test annotation generation
+ - Added unit test annotation inspection
+
+
+ Previous Versions
+
+ - Initial PITest integration
+ - Bug fixes and stability improvements
+
+ ]]>
+
+ 1.0.10
Jason Lam
- com.github.jaksonlin.pitestintellij
- Pitest-Gradle
- jaksonlin
+ com.github.jaksonlin.testcraftpro
+ TestCraft Pro
+ Jakson Lin
com.intellij.modules.platform
com.intellij.modules.java
com.intellij.modules.lang
- com.intellij.modules.json
+ com.intellij.modules.json
Git4Idea
messages.MyBundle
-
+
+ factoryClass="com.github.jaksonlin.testcraft.presentation.toolWindow.LLMSuggestionsToolWindowFactory"/>
+ instance="com.github.jaksonlin.testcraft.application.settings.TestCraftSettingsConfigurable"/>
-
-
-
+
+
+
+ implementationClass="com.github.jaksonlin.testcraft.application.completions.AnnotationCompletionContributor"/>
-
+
@@ -104,23 +164,29 @@
-
diff --git a/src/main/resources/META-INF/withJson.xml b/src/main/resources/META-INF/withJson.xml
deleted file mode 100644
index c4b32869..00000000
--- a/src/main/resources/META-INF/withJson.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
-
-
diff --git a/src/main/resources/messages/MyBundle.properties b/src/main/resources/messages/MyBundle.properties
index 4e0dfc19..109ac78c 100644
--- a/src/main/resources/messages/MyBundle.properties
+++ b/src/main/resources/messages/MyBundle.properties
@@ -82,6 +82,7 @@ toolwindow.llm.title=TestCraft LLM Suggestions
action.RunPitestAction.text=Run Mutation Test
action.RunCaseAnnoationCheckAction.text=Run Current Method Annotation Check
action.GenerateAnnotationCommandAction.text=Generate Testcase Annotation On Method
+action.CheckInvalidTestCasesAction.text=Check Invalid Test Cases
action.UnittestHelperToolMenu.text=TestCraft Unittest Helpers
action.UnittestHelperSubMenu.text=TestCraft Unittest Helper Tools
@@ -141,4 +142,8 @@ test.annotation.exists.title=Annotation Already Exists
# Pitest Run Messages
pitest.run.canceled=Pitest run was canceled
pitest.run.canceled.title=Canceled
-pitest.run.error=Error executing Pitest command: %s
+pitest.run.error=Error executing Pitest command: %
+pitest.view.report=View HTML Report
+
+# Chat
+chat.send.button=Send
diff --git a/src/main/resources/messages/MyBundle_en_US.properties b/src/main/resources/messages/MyBundle_en_US.properties
index 4e0dfc19..ada35522 100644
--- a/src/main/resources/messages/MyBundle_en_US.properties
+++ b/src/main/resources/messages/MyBundle_en_US.properties
@@ -142,3 +142,6 @@ test.annotation.exists.title=Annotation Already Exists
pitest.run.canceled=Pitest run was canceled
pitest.run.canceled.title=Canceled
pitest.run.error=Error executing Pitest command: %s
+pitest.view.report=View HTML Report
+# Chat
+chat.send.button=Send
diff --git a/src/main/resources/messages/MyBundle_zh_CN.properties b/src/main/resources/messages/MyBundle_zh_CN.properties
index deb4166c..1fd1b8a4 100644
--- a/src/main/resources/messages/MyBundle_zh_CN.properties
+++ b/src/main/resources/messages/MyBundle_zh_CN.properties
@@ -75,6 +75,7 @@ toolwindow.llm.title=TestCraft LLM 建议
action.RunPitestAction.text=运行变异测试
action.RunCaseAnnoationCheckAction.text=运行当前方法注解检查
action.GenerateAnnotationCommandAction.text=在方法上生成测试用例注解
+action.CheckInvalidTestCasesAction.text=检查无效测试用例
action.UnittestHelperToolMenu.text=TestCraft 单元测试助手
action.UnittestHelperSubMenu.text=TestCraft 单元测试辅助工具
@@ -149,3 +150,6 @@ test.annotation.exists.title=注解已存在
pitest.run.canceled=Pitest 运行已取消
pitest.run.canceled.title=已取消
pitest.run.error=执行 Pitest 命令时出错:%s
+pitest.view.report=查看 HTML 报告
+# Chat
+chat.send.button=发送