diff --git a/.run/Checks.run.xml b/.run/Checks.run.xml
new file mode 100644
index 0000000..77a8f51
--- /dev/null
+++ b/.run/Checks.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/.run/IDE.run.xml b/.run/IDE.run.xml
new file mode 100644
index 0000000..2e22010
--- /dev/null
+++ b/.run/IDE.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/.run/Plugin Verifier.run.xml b/.run/Plugin Verifier.run.xml
new file mode 100644
index 0000000..5ace28c
--- /dev/null
+++ b/.run/Plugin Verifier.run.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+ true
+ false
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a0eac5f..b4a5ab8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,16 @@
# Hacker News IDEA
## [Unreleased]
+
+### Added
+
+- `{by}` and `{id}` format specifiers.
+
+### Fixed
+
+- Change "Top Stories on Hacker News" to "Top Stories" as menu item title for popup.
+- Use java.net.http to fetch data, which allows specifying user agent.
+
## [1.3.0]
### Added
diff --git a/build.gradle.kts b/build.gradle.kts
index 282f443..6e7c8fc 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -39,6 +39,7 @@ tasks {
patchPluginXml {
version(properties("pluginVersion"))
sinceBuild(properties("pluginSinceBuild"))
+ untilBuild(properties("pluginUntilBuild"))
changeNotes(
closure {
diff --git a/gradle.properties b/gradle.properties
index 1182ac7..1ef2307 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -1,7 +1,8 @@
pluginGroup=sh.spinlock.idea.hackernews
pluginName=Hacker News
-pluginVersion=1.3.0
+pluginVersion=1.4.0
pluginSinceBuild=203
+pluginUntilBuild=300
pluginVerifierIdeVersions=2020.3.2, 2021.1
platformType=IC
platformVersion=2020.3
diff --git a/src/main/java/sh/spinlock/idea/hackernews/Configuration.java b/src/main/java/sh/spinlock/idea/hackernews/Configuration.java
index 66e8ae8..701c0a4 100644
--- a/src/main/java/sh/spinlock/idea/hackernews/Configuration.java
+++ b/src/main/java/sh/spinlock/idea/hackernews/Configuration.java
@@ -40,6 +40,7 @@ public void loadState(@NotNull State state) {
}
public static class State {
+
public Integer itemLimit = 30;
public String itemTextFormat = "{index}. {title}";
}
diff --git a/src/main/java/sh/spinlock/idea/hackernews/ConfigurationExtensionUI.java b/src/main/java/sh/spinlock/idea/hackernews/ConfigurationExtensionUI.java
index 0f766cf..ff3e381 100644
--- a/src/main/java/sh/spinlock/idea/hackernews/ConfigurationExtensionUI.java
+++ b/src/main/java/sh/spinlock/idea/hackernews/ConfigurationExtensionUI.java
@@ -14,7 +14,6 @@
import javax.swing.JPanel;
public class ConfigurationExtensionUI {
-
public JPanel panel;
public ComboBox itemLimitSelector;
public JBTextField itemTextFormat;
diff --git a/src/main/java/sh/spinlock/idea/hackernews/HackerNewsAction.java b/src/main/java/sh/spinlock/idea/hackernews/HackerNewsAction.java
index b7b62cd..350e4c4 100644
--- a/src/main/java/sh/spinlock/idea/hackernews/HackerNewsAction.java
+++ b/src/main/java/sh/spinlock/idea/hackernews/HackerNewsAction.java
@@ -3,9 +3,10 @@
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.components.ServiceManager;
+import com.intellij.openapi.project.DumbAware;
import org.jetbrains.annotations.NotNull;
-public abstract class HackerNewsAction extends AnAction {
+public abstract class HackerNewsAction extends AnAction implements DumbAware {
@Override
public void update(@NotNull AnActionEvent e) {
e.getPresentation().setEnabled(true);
diff --git a/src/main/java/sh/spinlock/idea/hackernews/ItemPopupStep.java b/src/main/java/sh/spinlock/idea/hackernews/ItemPopupStep.java
index 1ecfe1d..dd88fa7 100644
--- a/src/main/java/sh/spinlock/idea/hackernews/ItemPopupStep.java
+++ b/src/main/java/sh/spinlock/idea/hackernews/ItemPopupStep.java
@@ -25,12 +25,16 @@ public ItemPopupStep(Configuration configuration, String title, List items = HackerNewsClient.loadTopStories(configuration.getItemLimit());
+ List items =
+ HackerNewsClient.shared().loadTopStories(configuration.getItemLimit());
ListPopup popup =
JBPopupFactory.getInstance()
diff --git a/src/main/java/sh/spinlock/idea/hackernews/client/HackerNewsClient.java b/src/main/java/sh/spinlock/idea/hackernews/client/HackerNewsClient.java
index 5e9235e..ff66627 100644
--- a/src/main/java/sh/spinlock/idea/hackernews/client/HackerNewsClient.java
+++ b/src/main/java/sh/spinlock/idea/hackernews/client/HackerNewsClient.java
@@ -4,7 +4,11 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.intellij.openapi.util.Pair;
import java.io.IOException;
-import java.net.URL;
+import java.net.URI;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@@ -16,14 +20,27 @@ public class HackerNewsClient {
public static final String ITEM_BASE_URL = String.format("%s/item", BASE_URL);
public static final String TOP_STORIES_URL = String.format("%s/topstories.json", BASE_URL);
- private static final ObjectMapper mapper =
+ private static final @NotNull HackerNewsClient SHARED =
+ new HackerNewsClient(HttpClient.newHttpClient());
+
+ public static @NotNull HackerNewsClient shared() {
+ return SHARED;
+ }
+
+ private final @NotNull HttpClient httpClient;
+ private final @NotNull ObjectMapper mapper =
new ObjectMapper().configure(DeserializationFeature.FAIL_ON_IGNORED_PROPERTIES, false);
+ public HackerNewsClient(@NotNull HttpClient httpClient) {
+ this.httpClient = httpClient;
+ }
+
@NotNull
- public static List getTopStories() {
+ public List getTopStoriesIds() {
try {
+ String content = fetch(TOP_STORIES_URL);
List items = new ArrayList<>();
- for (Object object : mapper.readValue(new URL(TOP_STORIES_URL), List.class)) {
+ for (Object object : mapper.readValue(content, List.class)) {
if (object instanceof Integer) {
items.add((Integer) object);
}
@@ -34,9 +51,8 @@ public static List getTopStories() {
}
}
- @NotNull
- public static List loadTopStories(int limit) {
- List stories = HackerNewsClient.getTopStories();
+ public @NotNull List<@NotNull HackerNewsItem> loadTopStories(int limit) {
+ List stories = getTopStoriesIds();
return stories.stream()
.limit(limit)
@@ -44,7 +60,7 @@ public static List loadTopStories(int limit) {
.parallel()
.map(
(pair) -> {
- HackerNewsItem item = HackerNewsClient.getItem(pair.getSecond());
+ HackerNewsItem item = getStoryItem(pair.getSecond());
item.indexInList = pair.first;
return item;
})
@@ -52,13 +68,33 @@ public static List loadTopStories(int limit) {
.collect(Collectors.toList());
}
- @NotNull
- public static HackerNewsItem getItem(int id) {
+ public @NotNull HackerNewsItem getStoryItem(int id) {
try {
- return mapper.readValue(
- new URL(String.format("%s/%s.json", ITEM_BASE_URL, id)), HackerNewsItem.class);
+ String content = fetch(String.format("%s/%s.json", ITEM_BASE_URL, id));
+ return mapper.readValue(content, HackerNewsItem.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
+
+ private @NotNull String fetch(@NotNull String url) {
+ try {
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .GET()
+ .uri(URI.create(url))
+ .header("User-Agent", "github.com/SpinlockLabs/idea-hacker-news")
+ .build();
+ HttpResponse response = httpClient.send(request, BodyHandlers.ofString());
+
+ if (response.statusCode() != 200) {
+ throw new RuntimeException(
+ String.format("Failed to fetch URL %s (status code: %d)", url, response.statusCode()));
+ }
+
+ return response.body();
+ } catch (IOException | InterruptedException e) {
+ throw new RuntimeException("Failed to fetch URL " + url, e);
+ }
+ }
}
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 778fe77..dfc7422 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -39,7 +39,7 @@