Skip to content

Commit 11668c0

Browse files
authored
Merge pull request #114 from kdbinsidebrains/113-qspec-integration
113 qspec integration
2 parents 24ae4f6 + 1838adb commit 11668c0

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2911
-523
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# KdbInsideBrains Changelog
22

3+
## [5.15.0]
4+
5+
### Added
6+
7+
- QSpec Testing Framework added: https://www.kdbinsidebrains.dev/testing)
8+
- Run local KDB Instance redesigned
9+
10+
### Fixed
11+
12+
- InstancesTree freezes in 2025.x version
13+
314
## [5.14.3]
415

516
### Fixed

src/main/java/icons/KdbIcons.java

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ public final class KdbIcons {
1414

1515
private static @NotNull Icon row(@NotNull Icon icon1, @NotNull Icon icon2) {
1616
return IconManager.getInstance().createRowIcon(icon1, icon2);
17-
// LayeredIcon icon = new LayeredIcon(2);
18-
// icon.setIcon(icon1, 0, 0, 0);
19-
// icon.setIcon(icon2, 1, icon1.getIconWidth(), 0);
20-
// return icon;
2117
}
2218

2319
private static @NotNull Icon layer(@NotNull Icon icon1, @NotNull Icon icon2) {
@@ -27,13 +23,16 @@ public final class KdbIcons {
2723
public static final class Main {
2824
public static final @NotNull Icon File = load("/org/kdb/inside/brains/icons/qFile.svg");
2925
public static final @NotNull Icon Module = load("/org/kdb/inside/brains/icons/q.svg");
30-
public static final @NotNull Icon Library = load("/org/kdb/inside/brains/icons/q.svg");
31-
public static final @NotNull Icon Application = load("/org/kdb/inside/brains/icons/q.svg");
32-
public static final @NotNull Icon Notification = load("/org/kdb/inside/brains/icons/q.svg");
26+
public static final @NotNull Icon Library = Module;
27+
public static final @NotNull Icon Application = Module;
28+
public static final @NotNull Icon Notification = Module;
3329

3430
public static final @NotNull Icon ToolWindow = load("/org/kdb/inside/brains/icons/windows/instances.svg");
3531
public static final @NotNull Icon ConsoleWindow = load("/org/kdb/inside/brains/icons/windows/console.svg");
3632
public static final @NotNull Icon InspectorWindow = load("/org/kdb/inside/brains/icons/windows/inspector.svg");
33+
34+
public static final @NotNull Icon RunFile = Application;
35+
public static final @NotNull Icon RunQSpec = layer(RunFile, AllIcons.RunConfigurations.TestMark);
3736
}
3837

3938
public static final class Scope {

src/main/java/org/kdb/inside/brains/QFileType.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
import javax.swing.*;
1515
import java.io.File;
16+
import java.nio.file.Path;
1617

1718
public final class QFileType extends LanguageFileType {
1819
public static final QFileType INSTANCE = new QFileType();
@@ -41,6 +42,10 @@ public Icon getIcon() {
4142
return "q";
4243
}
4344

45+
public static boolean hasExtension(Path file) {
46+
return hasExtension(file.getFileName().toString());
47+
}
48+
4449
public static boolean hasExtension(File file) {
4550
return hasExtension(file.getName());
4651
}
@@ -60,6 +65,10 @@ public static QFile createFactoryFile(Project project, String text) {
6065
return (QFile) PsiFileFactory.getInstance(project).createFileFromText("QElementFactory.q", QFileType.INSTANCE, text);
6166
}
6267

68+
public static boolean is(@Nullable Path file) {
69+
return file != null && hasExtension(file.toFile());
70+
}
71+
6372
public static boolean is(@Nullable PsiFile file) {
6473
return (file instanceof QFile) || (file != null && is(file.getVirtualFile()));
6574
}

src/main/java/org/kdb/inside/brains/UIUtils.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,37 @@
55
import com.intellij.openapi.actionSystem.DataContext;
66
import com.intellij.openapi.application.ApplicationManager;
77
import com.intellij.openapi.application.Experiments;
8+
import com.intellij.openapi.fileChooser.FileChooserDescriptor;
89
import com.intellij.openapi.project.DumbAwareAction;
910
import com.intellij.openapi.project.Project;
10-
import com.intellij.openapi.ui.InputValidator;
11-
import com.intellij.openapi.ui.Messages;
11+
import com.intellij.openapi.ui.*;
1212
import com.intellij.openapi.ui.popup.ComponentPopupBuilder;
1313
import com.intellij.openapi.ui.popup.JBPopup;
1414
import com.intellij.openapi.ui.popup.JBPopupFactory;
1515
import com.intellij.openapi.util.TextRange;
1616
import com.intellij.ui.ContextHelpLabel;
1717
import com.intellij.ui.DocumentAdapter;
18+
import com.intellij.ui.TextFieldWithHistoryWithBrowseButton;
1819
import com.intellij.ui.components.JBTextField;
1920
import com.intellij.ui.content.Content;
2021
import com.intellij.ui.content.ContentFactory;
2122
import com.intellij.ui.tabs.TabInfo;
2223
import com.intellij.util.ui.ImageUtil;
2324
import org.apache.commons.lang.text.StrSubstitutor;
2425
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
2527

2628
import javax.swing.*;
2729
import javax.swing.event.DocumentEvent;
2830
import java.awt.*;
2931
import java.awt.event.KeyEvent;
3032
import java.awt.image.BufferedImage;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
3135
import java.util.function.Consumer;
36+
import java.util.function.Function;
3237
import java.util.function.Predicate;
38+
import java.util.function.Supplier;
3339

3440
public final class UIUtils {
3541
public static final String KEY_COLUMN_PREFIX = "\u00A1";
@@ -192,4 +198,47 @@ public void actionPerformed(@NotNull AnActionEvent e) {
192198
}.registerCustomShortcutSet(new CustomShortcutSet(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)), textField);
193199
return popup;
194200
}
201+
202+
public static void initializerTextBrowseValidator(@NotNull TextFieldWithBrowseButton field, @NotNull Supplier<String> emptySupplier, @Nullable Supplier<String> nonExistSupplier) {
203+
initializerTextBrowseValidator(field, emptySupplier, nonExistSupplier, (Function<String, String>[]) null);
204+
}
205+
206+
@SafeVarargs
207+
public static void initializerTextBrowseValidator(@NotNull TextFieldWithBrowseButton field, @NotNull Supplier<String> emptySupplier, @Nullable Supplier<String> nonExistSupplier, Function<String, String>... customValidator) {
208+
final JTextField textField = field.getTextField();
209+
210+
final ComponentValidator componentValidator = new ComponentValidator(field);
211+
componentValidator.withValidator(() -> {
212+
final String text = textField.getText().trim();
213+
if (text.isEmpty()) {
214+
return new ValidationInfo(emptySupplier.get(), textField);
215+
}
216+
if (nonExistSupplier != null && !Files.exists(Path.of(text))) {
217+
return new ValidationInfo(nonExistSupplier.get(), textField);
218+
}
219+
if (customValidator != null) {
220+
for (Function<String, String> validator : customValidator) {
221+
final String apply = validator.apply(text);
222+
if (apply != null) {
223+
return new ValidationInfo(apply, textField);
224+
}
225+
}
226+
}
227+
return null;
228+
}).andRegisterOnDocumentListener(textField).installOn(textField);
229+
}
230+
231+
public static void initializeFileChooser(@Nullable Project project, @NotNull ComponentWithBrowseButton<?> field, @NotNull FileChooserDescriptor descriptor) {
232+
if (descriptor.getRoots().isEmpty() && project != null) {
233+
descriptor.withRoots(project.getBaseDir());
234+
}
235+
236+
if (field instanceof TextFieldWithBrowseButton button) {
237+
button.addBrowseFolderListener(new TextBrowseFolderListener(descriptor, project));
238+
} else if (field instanceof TextFieldWithHistoryWithBrowseButton button) {
239+
button.addBrowseFolderListener(descriptor.getTitle(), descriptor.getDescription(), project, descriptor, TextComponentAccessors.TEXT_FIELD_WITH_HISTORY_WHOLE_TEXT);
240+
} else {
241+
throw new UnsupportedOperationException("Unsupported field type: " + field.getClass());
242+
}
243+
}
195244
}

src/main/java/org/kdb/inside/brains/core/KdbQuery.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public KdbQuery(String expr, Object... args) {
1616
this.args = args;
1717
}
1818

19-
Object toQueryObject(boolean normalize) {
19+
public Object toQueryObject(boolean normalize) {
2020
final char[] q = (normalize ? normalizeQuery(expr) : expr).toCharArray();
2121
if (args == null) {
2222
return q;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.kdb.inside.brains.ide.qspec;
2+
3+
import org.jdom.Element;
4+
5+
import java.nio.file.Files;
6+
import java.nio.file.Path;
7+
import java.util.Objects;
8+
9+
// https://github.com/nugend/qspec
10+
public record QSpecLibrary(String specFolder, String script) {
11+
public static QSpecLibrary read(Element e) {
12+
final Element child = e.getChild("qspec_library");
13+
if (child == null) {
14+
return null;
15+
}
16+
return new QSpecLibrary(child.getAttributeValue("path"), child.getText());
17+
}
18+
19+
public static void validate(String path) throws IllegalStateException {
20+
final boolean exists = Files.exists(Path.of(path).resolve("lib/init.q"));
21+
if (!exists) {
22+
throw new IllegalStateException("QSpec folder doesn't have lib/init.q inside");
23+
}
24+
}
25+
26+
public QSpecLibrary validate() throws IllegalStateException {
27+
validate(specFolder);
28+
return this;
29+
}
30+
31+
public void write(Element element) {
32+
final Element l = new Element("qspec_library");
33+
l.setAttribute("path", specFolder);
34+
l.setText(script);
35+
element.addContent(l);
36+
}
37+
38+
@Override
39+
public boolean equals(Object o) {
40+
if (!(o instanceof QSpecLibrary library)) return false;
41+
return Objects.equals(specFolder, library.specFolder) && Objects.equals(script, library.script);
42+
}
43+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.kdb.inside.brains.ide.qspec.QSpecLibraryPanel">
3+
<grid id="27dc6" binding="myComponent" layout-manager="GridLayoutManager" row-count="4" column-count="3" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
4+
<margin top="0" left="0" bottom="0" right="0"/>
5+
<constraints>
6+
<xy x="20" y="20" width="674" height="326"/>
7+
</constraints>
8+
<properties/>
9+
<border type="none"/>
10+
<children>
11+
<component id="3587f" class="javax.swing.JLabel">
12+
<constraints>
13+
<grid row="1" column="0" row-span="1" col-span="3" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
14+
</constraints>
15+
<properties>
16+
<text value="Custom Configuration Script:"/>
17+
</properties>
18+
</component>
19+
<component id="c4568" class="javax.swing.JLabel">
20+
<constraints>
21+
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
22+
</constraints>
23+
<properties>
24+
<labelFor value=""/>
25+
<text value="QSpec Path"/>
26+
</properties>
27+
</component>
28+
<component id="bafdc" class="com.intellij.openapi.ui.TextFieldWithBrowseButton" binding="specFolderField">
29+
<constraints>
30+
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="3" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false">
31+
<preferred-size width="298" height="34"/>
32+
</grid>
33+
</constraints>
34+
<properties/>
35+
</component>
36+
<component id="dd52" class="javax.swing.JButton" binding="specDownloadButton">
37+
<constraints>
38+
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="4" fill="0" indent="0" use-parent-layout="false"/>
39+
</constraints>
40+
<properties>
41+
<text value="GitHub Download"/>
42+
</properties>
43+
</component>
44+
<vspacer id="abf57">
45+
<constraints>
46+
<grid row="3" column="0" row-span="1" col-span="3" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
47+
</constraints>
48+
</vspacer>
49+
<scrollpane id="6184d" binding="scriptPanel">
50+
<constraints>
51+
<grid row="2" column="0" row-span="1" col-span="3" vsize-policy="7" hsize-policy="7" anchor="0" fill="3" indent="0" use-parent-layout="false">
52+
<minimum-size width="-1" height="160"/>
53+
<preferred-size width="-1" height="160"/>
54+
</grid>
55+
</constraints>
56+
<properties/>
57+
<border type="none"/>
58+
<children/>
59+
</scrollpane>
60+
</children>
61+
</grid>
62+
</form>
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package org.kdb.inside.brains.ide.qspec;
2+
3+
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory;
4+
import com.intellij.openapi.project.Project;
5+
import com.intellij.openapi.ui.TextFieldWithBrowseButton;
6+
import com.intellij.openapi.vfs.VfsUtil;
7+
import com.intellij.ui.LanguageTextField;
8+
import org.jetbrains.annotations.Nullable;
9+
import org.kdb.inside.brains.QLanguage;
10+
import org.kdb.inside.brains.UIUtils;
11+
12+
import javax.swing.*;
13+
14+
public class QSpecLibraryPanel {
15+
private static final String DEFAULT_TEXT = """
16+
/ Q below code that will be executed before running any QSpec tests.
17+
/ You can use it to initialize any required variables or load external files
18+
/ Please use only system commands, not slashed one. Use semicolons to split sentences.
19+
""";
20+
private JPanel myComponent;
21+
private JScrollPane scriptPanel;
22+
private LanguageTextField scriptTextArea;
23+
private JButton specDownloadButton;
24+
private TextFieldWithBrowseButton specFolderField;
25+
26+
public QSpecLibraryPanel() {
27+
}
28+
29+
public JComponent init(@Nullable Project project, @Nullable QSpecLibrary library) {
30+
scriptTextArea = new LanguageTextField(QLanguage.INSTANCE, project, DEFAULT_TEXT, false);
31+
32+
UIUtils.initializeFileChooser(project, specFolderField,
33+
FileChooserDescriptorFactory.createSingleFolderDescriptor()
34+
.withRoots(VfsUtil.getUserHomeDir())
35+
.withTitle("Nugend QSpec Directory")
36+
.withDescription("Select directory where Nugend QSpec package is stored")
37+
);
38+
39+
UIUtils.initializerTextBrowseValidator(specFolderField, () -> "Please select directory QSpec folder", () -> "QSpec directory doesn't exist", (s) -> {
40+
try {
41+
QSpecLibrary.validate(s);
42+
return null;
43+
} catch (Exception ex) {
44+
return ex.getMessage();
45+
}
46+
});
47+
specDownloadButton.addActionListener(e -> downloadQspec(project, specFolderField));
48+
49+
scriptPanel.setViewportView(scriptTextArea);
50+
51+
setLibrary(library);
52+
53+
return myComponent;
54+
}
55+
56+
private void downloadQspec(@Nullable Project project, TextFieldWithBrowseButton field) {
57+
final QSpecModuleDownloadDialog dialog = new QSpecModuleDownloadDialog(project);
58+
if (dialog.showAndGet()) {
59+
field.setText(dialog.getDownloadedPath().toAbsolutePath().toString());
60+
}
61+
}
62+
63+
public QSpecLibrary getLibrary() {
64+
return new QSpecLibrary(specFolderField.getText(), scriptTextArea.getText());
65+
}
66+
67+
public void setLibrary(QSpecLibrary library) {
68+
if (library != null) {
69+
scriptTextArea.setText(library.script());
70+
specFolderField.setText(library.specFolder());
71+
} else {
72+
specFolderField.setText("");
73+
scriptTextArea.setText(DEFAULT_TEXT);
74+
}
75+
}
76+
77+
public void setEnabled(boolean enabled) {
78+
specFolderField.setEnabled(enabled);
79+
specDownloadButton.setEnabled(enabled);
80+
scriptTextArea.setEnabled(enabled);
81+
}
82+
}

0 commit comments

Comments
 (0)