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