├── .github
└── workflows
│ └── gradle.yml
├── .gitignore
├── .travis.yml
├── README.md
├── build.gradle
├── changeNotes.txt
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── libs
└── github-api-1.309-SNAPSHOT.jar
├── settings.gradle
└── src
└── main
├── java
└── com
│ └── chuntung
│ └── plugin
│ └── gistsnippet
│ ├── action
│ ├── AddAction.java
│ ├── CustomComboBoxAction.java
│ ├── DeleteAction.java
│ ├── EditAction.java
│ ├── InsertAction.java
│ ├── OpenInBrowserAction.java
│ └── ReloadAction.java
│ ├── dto
│ ├── FileNodeDTO.java
│ ├── ScopeEnum.java
│ ├── SnippetNodeDTO.java
│ └── SnippetRootNode.java
│ ├── service
│ ├── GistException.java
│ ├── GistSnippetService.java
│ ├── GithubAccountHolder.java
│ └── GithubHelper.java
│ └── view
│ ├── CustomActionLink.java
│ ├── CustomDropDownLink.java
│ ├── CustomTreeStructure.java
│ ├── InsertGistDialog.form
│ └── InsertGistDialog.java
└── resources
├── META-INF
├── plugin.xml
└── pluginIcon.svg
└── images
├── gist.png
├── own.png
├── public.png
├── secret.png
├── starred.png
└── unstarred.png
/.github/workflows/gradle.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will build a Java project with Gradle and cache/restore any dependencies to improve the workflow execution time
6 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-gradle
7 |
8 | name: Java CI with Gradle
9 |
10 | on:
11 | push:
12 | branches: [ "master" ]
13 | pull_request:
14 | branches: [ "master" ]
15 |
16 | jobs:
17 | build:
18 |
19 | runs-on: ubuntu-latest
20 | permissions:
21 | contents: read
22 |
23 | steps:
24 | - uses: actions/checkout@v4
25 | - name: Set up JDK 17
26 | uses: actions/setup-java@v4
27 | with:
28 | java-version: '17'
29 | distribution: 'temurin'
30 |
31 | # Configure Gradle for optimal use in GiHub Actions, including caching of downloaded dependencies.
32 | # See: https://github.com/gradle/actions/blob/main/setup-gradle/README.md
33 | - name: Setup Gradle
34 | uses: gradle/actions/setup-gradle@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
35 |
36 | - name: Build with Gradle Wrapper
37 | run: ./gradlew buildPlugin
38 |
39 | - name: Upload plugin file
40 | uses: actions/upload-artifact@v4
41 | with:
42 | retention-days: 1
43 | name: plugin-file
44 | path: build/distributions/*.zip
45 |
46 | dependency-submission:
47 |
48 | runs-on: ubuntu-latest
49 | permissions:
50 | contents: write
51 |
52 | steps:
53 | - uses: actions/checkout@v4
54 | - name: Set up JDK 17
55 | uses: actions/setup-java@v4
56 | with:
57 | java-version: '17'
58 | distribution: 'temurin'
59 |
60 | # Generates and submits a dependency graph, enabling Dependabot Alerts for all project dependencies.
61 | # See: https://github.com/gradle/actions/blob/main/dependency-submission/README.md
62 | - name: Generate and submit dependency graph
63 | uses: gradle/actions/dependency-submission@417ae3ccd767c252f5661f1ace9f835f9654f2b5 # v3.1.0
64 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle/
2 | .idea/
3 | build/
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | jdk:
4 | - openjdk8
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gist Snippet
2 |
3 | A code snippet tool based on GitHub gist, which provides with a feature to fetch secret or starred gist of GitHub accounts. It depends on built-in GitHub plugin which should be enabled.
4 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | jcenter()
5 | }
6 | }
7 |
8 | plugins {
9 | id 'org.jetbrains.intellij' version '1.10.2'
10 | }
11 |
12 | group 'com.chuntung.plugin'
13 | version "${version}"
14 |
15 | sourceCompatibility = JavaVersion.VERSION_1_8
16 | [compileJava,compileTestJava,javadoc]*.options*.encoding = 'UTF-8'
17 |
18 | repositories {
19 | mavenLocal()
20 | mavenCentral()
21 | }
22 |
23 | dependencies {
24 | // https://mvnrepository.com/artifact/org.kohsuke/github-api
25 | implementation(files("libs/github-api-1.309-SNAPSHOT.jar"))
26 |
27 | testImplementation group: 'junit', name: 'junit', version: '4.12'
28 | }
29 |
30 | // See https://github.com/JetBrains/gradle-intellij-plugin/
31 | intellij {
32 | pluginName = "Gist Snippet"
33 | version = ideaVersion
34 | plugins = ["org.jetbrains.plugins.github"]
35 | sameSinceUntilBuild = Boolean.valueOf(isEAP)
36 | patchPluginXml {
37 | untilBuild = customUtilBuild
38 | changeNotes = "${new File('changeNotes.txt').getText('UTF-8')}"
39 | }
40 | }
--------------------------------------------------------------------------------
/changeNotes.txt:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | version = 1.2.0
2 | ideaVersion = IC-2023.1
3 | customUtilBuild = 299.*
4 | isEAP = false
5 | pluginChannels = nightly
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Mon Apr 29 10:48:50 CST 2024
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS='"-Xmx64m"'
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS="-Xmx64m"
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/libs/github-api-1.309-SNAPSHOT.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/libs/github-api-1.309-SNAPSHOT.jar
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'gist-snippet'
2 |
3 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/AddAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.intellij.openapi.actionSystem.ActionUpdateThread;
8 | import com.intellij.openapi.actionSystem.AnAction;
9 | import com.intellij.openapi.actionSystem.AnActionEvent;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 |
13 | /**
14 | * Open add dialog to create gist.
15 | */
16 | public class AddAction extends AnAction {
17 | @Override
18 | public void actionPerformed(@NotNull AnActionEvent e) {
19 | // TODO check selected file or path or text, pass them to dialog
20 | }
21 |
22 | public @NotNull ActionUpdateThread getActionUpdateThread() {
23 | return ActionUpdateThread.EDT;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/CustomComboBoxAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.intellij.openapi.actionSystem.*;
8 | import com.intellij.openapi.actionSystem.ex.ComboBoxAction;
9 | import com.intellij.openapi.project.DumbAware;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | import javax.swing.*;
13 |
14 | /**
15 | * ComboBox Action to be used in toolbar.
16 | */
17 | public class CustomComboBoxAction extends ComboBoxAction implements DumbAware {
18 | private AnAction[] actions;
19 | private String myText;
20 | private Icon myIcon;
21 |
22 | public static CustomComboBoxAction create(AnAction... actions) {
23 | return new CustomComboBoxAction(actions);
24 | }
25 |
26 | public CustomComboBoxAction(AnAction... actions) {
27 | this.actions = actions;
28 | // set first action
29 | if (actions != null && actions.length > 0) {
30 | reset();
31 | }
32 | }
33 |
34 | public @NotNull ActionUpdateThread getActionUpdateThread() {
35 | return ActionUpdateThread.EDT;
36 | }
37 |
38 | public void update(@NotNull AnActionEvent e) {
39 | if (e.getPresentation() != null) {
40 | e.getPresentation().setText(myText);
41 | e.getPresentation().setIcon(myIcon);
42 | }
43 | }
44 |
45 | public String getText() {
46 | return myText;
47 | }
48 |
49 | public void reset() {
50 | Presentation first = actions[0].getTemplatePresentation();
51 | myText = first.getText();
52 | myIcon = first.getIcon();
53 | }
54 |
55 | @NotNull
56 | @Override
57 | protected DefaultActionGroup createPopupActionGroup(JComponent button) {
58 | DefaultActionGroup group = new DefaultActionGroup();
59 | for (AnAction action : actions) {
60 | group.add(new DelegatedAction(action));
61 | }
62 | return group;
63 | }
64 |
65 | private class DelegatedAction extends AnAction {
66 | private AnAction target;
67 |
68 | DelegatedAction(AnAction target) {
69 | this.target = target;
70 | getTemplatePresentation().setText(target.getTemplatePresentation().getText());
71 | getTemplatePresentation().setIcon(target.getTemplatePresentation().getIcon());
72 | }
73 |
74 | @Override
75 | public void actionPerformed(@NotNull AnActionEvent e) {
76 | target.actionPerformed(e);
77 | // update combox text and icon
78 | myIcon = e.getPresentation().getIcon();
79 | myText = e.getPresentation().getText();
80 | }
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/DeleteAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.chuntung.plugin.gistsnippet.dto.ScopeEnum;
8 | import com.chuntung.plugin.gistsnippet.dto.SnippetNodeDTO;
9 | import com.chuntung.plugin.gistsnippet.dto.SnippetRootNode;
10 | import com.chuntung.plugin.gistsnippet.service.GistSnippetService;
11 | import com.chuntung.plugin.gistsnippet.service.GithubAccountHolder;
12 | import com.intellij.openapi.actionSystem.ActionUpdateThread;
13 | import com.intellij.openapi.actionSystem.AnAction;
14 | import com.intellij.openapi.actionSystem.AnActionEvent;
15 | import com.intellij.openapi.progress.ProgressIndicator;
16 | import com.intellij.openapi.progress.Task;
17 | import com.intellij.openapi.project.DumbAware;
18 | import com.intellij.openapi.project.Project;
19 | import com.intellij.openapi.ui.MessageDialogBuilder;
20 | import com.intellij.ui.tree.StructureTreeModel;
21 | import org.jetbrains.annotations.NotNull;
22 |
23 | import javax.swing.*;
24 | import javax.swing.tree.DefaultMutableTreeNode;
25 | import javax.swing.tree.TreePath;
26 | import java.util.ArrayList;
27 | import java.util.HashSet;
28 | import java.util.List;
29 | import java.util.Set;
30 |
31 | /**
32 | * Delete selected own gists.
33 | */
34 | public class DeleteAction extends AnAction implements DumbAware {
35 | private JTree tree;
36 | private StructureTreeModel structure;
37 | private SnippetRootNode root;
38 | private Project project;
39 |
40 | public DeleteAction(JTree tree, StructureTreeModel structure, SnippetRootNode root, Project project) {
41 | super("Delete", "Delete selected gists", null);
42 | this.tree = tree;
43 | this.structure = structure;
44 | this.root = root;
45 | this.project = project;
46 | }
47 |
48 | @Override
49 | public void actionPerformed(@NotNull AnActionEvent e) {
50 | List gistIds = new ArrayList<>();
51 | Set gists = new HashSet<>();
52 | StringBuilder msgs = new StringBuilder();
53 | TreePath[] paths = tree.getSelectionPaths();
54 | for (TreePath path : paths) {
55 | Object userObject = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
56 | if (userObject instanceof SnippetNodeDTO) {
57 | SnippetNodeDTO node = (SnippetNodeDTO) userObject;
58 | if (node.getScope().equals(ScopeEnum.OWN)) {
59 | gists.add(node);
60 | gistIds.add(node.getId());
61 | msgs.append('\n').append(node.getDescription() == null ? node.getId() : node.getDescription());
62 | }
63 | }
64 | }
65 |
66 | boolean yes = MessageDialogBuilder.yesNo("Delete", "Delete selected gists?" + msgs).isYes();
67 | if (yes) {
68 | new Task.Backgroundable(project, "Deleting gists...") {
69 | private boolean deleted = false;
70 |
71 | @Override
72 | public void run(@NotNull ProgressIndicator indicator) {
73 | String token = GithubAccountHolder.getInstance(project).getAccessToken();
74 | GistSnippetService service = GistSnippetService.getInstance();
75 | service.deleteGist(token, gistIds);
76 | deleted = true;
77 | }
78 |
79 | @Override
80 | public void onSuccess() {
81 | // refresh tree
82 | if (deleted) {
83 | root.children().removeAll(gists);
84 | structure.invalidate();
85 | }
86 | }
87 | }.queue();
88 | }
89 | }
90 |
91 | @Override
92 | public void update(@NotNull AnActionEvent event) {
93 | TreePath[] paths = tree.getSelectionPaths();
94 | if (paths != null) {
95 | for (TreePath path : paths) {
96 | Object userObject = ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject();
97 | if (userObject instanceof SnippetNodeDTO) {
98 | SnippetNodeDTO node = (SnippetNodeDTO) userObject;
99 | if (node.getScope().equals(ScopeEnum.OWN)) {
100 | return;
101 | }
102 | }
103 | }
104 | }
105 |
106 | event.getPresentation().setVisible(false);
107 | }
108 |
109 | public @NotNull ActionUpdateThread getActionUpdateThread() {
110 | return ActionUpdateThread.EDT;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/EditAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.intellij.openapi.actionSystem.ActionUpdateThread;
8 | import com.intellij.openapi.actionSystem.AnAction;
9 | import com.intellij.openapi.actionSystem.AnActionEvent;
10 | import org.jetbrains.annotations.NotNull;
11 |
12 | /**
13 | * Edit the first gist in the selection.
14 | */
15 | public class EditAction extends AnAction {
16 | @Override
17 | public void actionPerformed(@NotNull AnActionEvent e) {
18 |
19 | }
20 |
21 | public @NotNull ActionUpdateThread getActionUpdateThread() {
22 | return ActionUpdateThread.EDT;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/InsertAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.chuntung.plugin.gistsnippet.view.InsertGistDialog;
8 | import com.intellij.icons.AllIcons;
9 | import com.intellij.openapi.actionSystem.ActionUpdateThread;
10 | import com.intellij.openapi.actionSystem.AnAction;
11 | import com.intellij.openapi.actionSystem.AnActionEvent;
12 | import com.intellij.openapi.actionSystem.CommonDataKeys;
13 | import com.intellij.openapi.application.ApplicationManager;
14 | import com.intellij.openapi.command.CommandProcessor;
15 | import com.intellij.openapi.editor.Document;
16 | import com.intellij.openapi.editor.Editor;
17 | import com.intellij.openapi.editor.ScrollType;
18 | import com.intellij.openapi.editor.SelectionModel;
19 | import com.intellij.openapi.project.DumbAware;
20 | import com.intellij.openapi.project.Project;
21 | import com.intellij.openapi.util.IconLoader;
22 | import org.jetbrains.annotations.NotNull;
23 |
24 | /**
25 | * Open dialog to select gist to insert into editor.
26 | */
27 | public class InsertAction extends AnAction implements DumbAware {
28 | public InsertAction() {
29 | super(IconLoader.getIcon("/images/gist.png", InsertAction.class));
30 | }
31 |
32 | @Override
33 | public void actionPerformed(@NotNull AnActionEvent event) {
34 | Project project = event.getProject();
35 | Editor editor = event.getData(CommonDataKeys.EDITOR);
36 | if (editor == null) {
37 | return;
38 | }
39 |
40 | boolean writable = editor.getDocument().isWritable();
41 | InsertGistDialog dialog = new InsertGistDialog(project, writable);
42 |
43 | if (dialog.showAndGet()) {
44 | String selectedText = dialog.getSelectedText();
45 | if (selectedText == null) {
46 | return;
47 | }
48 |
49 | if (!writable) {
50 | return;
51 | }
52 |
53 | CommandProcessor.getInstance().executeCommand(project,
54 | () -> ApplicationManager.getApplication().runWriteAction(() -> insertIntoEditor(editor, selectedText)),
55 | "Insert Gist", null);
56 | }
57 | }
58 |
59 | private void insertIntoEditor(Editor editor, String selectedText) {
60 | Document document = editor.getDocument();
61 | SelectionModel selectionModel = editor.getSelectionModel();
62 | int offsetStart;
63 | if (selectionModel.hasSelection()) {
64 | offsetStart = selectionModel.getSelectionStart();
65 | int offsetEnd = selectionModel.getSelectionEnd();
66 | document.replaceString(offsetStart, offsetEnd, selectedText);
67 | } else {
68 | offsetStart = editor.getCaretModel().getOffset();
69 | document.insertString(offsetStart, selectedText);
70 | }
71 | int len = selectedText.length();
72 | selectionModel.setSelection(offsetStart, offsetStart + len);
73 | editor.getCaretModel().moveToOffset(offsetStart + len);
74 | editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
75 | }
76 |
77 | @Override
78 | public void update(@NotNull AnActionEvent event) {
79 | Editor editor = event.getData(CommonDataKeys.EDITOR);
80 | if (editor == null) {
81 | // only available for editor popup menu
82 | event.getPresentation().setVisible(false);
83 | }
84 | }
85 |
86 | public @NotNull ActionUpdateThread getActionUpdateThread() {
87 | return ActionUpdateThread.EDT;
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/OpenInBrowserAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.chuntung.plugin.gistsnippet.dto.FileNodeDTO;
8 | import com.chuntung.plugin.gistsnippet.dto.SnippetNodeDTO;
9 | import com.intellij.ide.BrowserUtil;
10 | import com.intellij.openapi.actionSystem.ActionUpdateThread;
11 | import com.intellij.openapi.actionSystem.AnActionEvent;
12 | import com.intellij.openapi.project.DumbAwareAction;
13 | import org.jetbrains.annotations.NotNull;
14 |
15 | import javax.swing.*;
16 | import javax.swing.tree.DefaultMutableTreeNode;
17 |
18 | /**
19 | * Open the first gist in the selection in browser.
20 | */
21 | public class OpenInBrowserAction extends DumbAwareAction {
22 | private JTree tree;
23 |
24 | public OpenInBrowserAction(JTree tree) {
25 | super("Open in browser");
26 | this.tree = tree;
27 | }
28 |
29 | @Override
30 | public void actionPerformed(@NotNull AnActionEvent e) {
31 | // Note, this will use the first item in the selection.
32 | Object selected = tree.getLastSelectedPathComponent();
33 | Object userObject = ((DefaultMutableTreeNode) selected).getUserObject();
34 | String url = null;
35 | if (userObject instanceof SnippetNodeDTO) {
36 | url = ((SnippetNodeDTO) userObject).getHtmlUrl();
37 | } else if (userObject instanceof FileNodeDTO) {
38 | url = ((FileNodeDTO) userObject).getRawUrl();
39 | }
40 | if (url != null) {
41 | BrowserUtil.open(url);
42 | }
43 | }
44 |
45 | public @NotNull ActionUpdateThread getActionUpdateThread() {
46 | return ActionUpdateThread.EDT;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/action/ReloadAction.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.action;
6 |
7 | import com.chuntung.plugin.gistsnippet.dto.FileNodeDTO;
8 | import com.intellij.openapi.actionSystem.ActionUpdateThread;
9 | import com.intellij.openapi.actionSystem.AnAction;
10 | import com.intellij.openapi.actionSystem.AnActionEvent;
11 | import com.intellij.openapi.project.DumbAware;
12 | import org.jetbrains.annotations.NotNull;
13 |
14 | import javax.swing.*;
15 | import javax.swing.tree.DefaultMutableTreeNode;
16 | import java.util.function.Consumer;
17 |
18 | /**
19 | * Reload selected gist.
20 | */
21 | public class ReloadAction extends AnAction implements DumbAware {
22 | private JTree tree;
23 | private Consumer consumer;
24 |
25 | public ReloadAction(JTree tree, Consumer consumer) {
26 | super("Reload", "Reload first selected gist file", null);
27 | this.tree = tree;
28 | this.consumer = consumer;
29 | }
30 |
31 | @Override
32 | public void actionPerformed(@NotNull AnActionEvent e) {
33 | consumer.accept(e);
34 | }
35 |
36 | @Override
37 | public void update(@NotNull AnActionEvent e) {
38 | // check if gist file selected
39 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
40 | if (!(node.getUserObject() instanceof FileNodeDTO)) {
41 | e.getPresentation().setVisible(false);
42 | }
43 | }
44 |
45 | public @NotNull ActionUpdateThread getActionUpdateThread() {
46 | return ActionUpdateThread.EDT;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/dto/FileNodeDTO.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.dto;
6 |
7 | import com.google.gson.annotations.SerializedName;
8 | import com.intellij.ide.projectView.PresentationData;
9 | import com.intellij.openapi.fileTypes.FileType;
10 | import com.intellij.openapi.fileTypes.FileTypeManager;
11 | import com.intellij.openapi.fileTypes.PlainTextFileType;
12 | import com.intellij.openapi.fileTypes.UnknownFileType;
13 | import com.intellij.ui.SimpleTextAttributes;
14 | import com.intellij.ui.treeStructure.SimpleNode;
15 | import org.jetbrains.annotations.NotNull;
16 | import org.kohsuke.github.GHGistFile;
17 |
18 | public class FileNodeDTO extends SimpleNode {
19 | private String filename;
20 | private String type;
21 | private String language;
22 |
23 | @SerializedName("raw_url")
24 | private String rawUrl;
25 |
26 | private Long size;
27 | private Boolean truncated;
28 | private String content;
29 |
30 | public FileNodeDTO() {
31 | }
32 |
33 | public FileNodeDTO(GHGistFile gistFile) {
34 | this.setFilename(gistFile.getFileName());
35 | this.setContent(gistFile.getContent());
36 | this.setLanguage(gistFile.getLanguage());
37 | this.setRawUrl(gistFile.getRawUrl());
38 | this.setType(gistFile.getType());
39 | this.setSize((long) gistFile.getSize());
40 | this.setTruncated(gistFile.isTruncated());
41 | }
42 |
43 | public String getFilename() {
44 | return filename;
45 | }
46 |
47 | public void setFilename(String filename) {
48 | this.filename = filename;
49 | }
50 |
51 | public String getType() {
52 | return type;
53 | }
54 |
55 | public void setType(String type) {
56 | this.type = type;
57 | }
58 |
59 | public String getLanguage() {
60 | return language;
61 | }
62 |
63 | public void setLanguage(String language) {
64 | this.language = language;
65 | }
66 |
67 | public String getRawUrl() {
68 | return rawUrl;
69 | }
70 |
71 | public void setRawUrl(String rawUrl) {
72 | this.rawUrl = rawUrl;
73 | }
74 |
75 | public Long getSize() {
76 | return size;
77 | }
78 |
79 | public void setSize(Long size) {
80 | this.size = size;
81 | }
82 |
83 | public Boolean getTruncated() {
84 | return truncated;
85 | }
86 |
87 | public void setTruncated(Boolean truncated) {
88 | this.truncated = truncated;
89 | }
90 |
91 | public String getContent() {
92 | return content;
93 | }
94 |
95 | public void setContent(String content) {
96 | this.content = content;
97 | }
98 |
99 | @NotNull
100 | @Override
101 | public SimpleNode[] getChildren() {
102 | return NO_CHILDREN;
103 | }
104 |
105 | @NotNull
106 | protected PresentationData createPresentation() {
107 | PresentationData presentation = new PresentationData();
108 | // file type icon
109 | FileType fileType = getFileType(this);
110 | setIcon(fileType.getIcon());
111 |
112 | String sizeTxt = " " + (size < 1024 ? (size + " B") : (size / 1024 + " KB"));
113 | presentation.addText(filename, SimpleTextAttributes.REGULAR_ATTRIBUTES);
114 | presentation.addText(sizeTxt, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES);
115 | return presentation;
116 | }
117 |
118 | @NotNull
119 | public static FileType getFileType(FileNodeDTO dto) {
120 | FileTypeManager fileTypeManager = FileTypeManager.getInstance();
121 | FileType fileType = fileTypeManager.getFileTypeByFileName(dto.getFilename());
122 | if (UnknownFileType.INSTANCE == fileType) {
123 | fileType = dto.getLanguage() == null ?
124 | PlainTextFileType.INSTANCE : fileTypeManager.getStdFileType(dto.getLanguage());
125 | }
126 | return fileType;
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/dto/ScopeEnum.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.dto;
6 |
7 | public enum ScopeEnum {
8 | OWN, STARRED, DISCOVER
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/dto/SnippetNodeDTO.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.dto;
6 |
7 | import com.intellij.ide.projectView.PresentationData;
8 | import com.intellij.openapi.util.IconLoader;
9 | import com.intellij.ui.SimpleTextAttributes;
10 | import com.intellij.ui.treeStructure.SimpleNode;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.kohsuke.github.GHGist;
13 | import org.kohsuke.github.GHGistFile;
14 | import org.kohsuke.github.GHUser;
15 |
16 | import javax.swing.*;
17 | import java.io.IOException;
18 | import java.net.MalformedURLException;
19 | import java.net.URL;
20 | import java.util.*;
21 | import java.util.concurrent.ConcurrentHashMap;
22 | import java.util.regex.Matcher;
23 | import java.util.regex.Pattern;
24 |
25 | public class SnippetNodeDTO extends SimpleNode {
26 | private static Map avatarCache = new ConcurrentHashMap<>();
27 | private Icon publicIcon = IconLoader.getIcon("/images/public.png", SnippetNodeDTO.class);
28 | private Icon secretIcon = IconLoader.getIcon("/images/secret.png", SnippetNodeDTO.class);
29 |
30 | public static final Pattern TITLE_PATTERN = Pattern.compile("#(.+)#");
31 | public static final Pattern TAG_PATTERN = Pattern.compile("\\[([^\\[\\]]+)\\]");
32 |
33 | private ScopeEnum scope;
34 | private boolean visible = true;
35 |
36 | private String id;
37 | private String title;
38 | private String htmlUrl;
39 | private String description;
40 | private Integer filesCount;
41 | private List files;
42 | private List tags;
43 | private GHUser owner;
44 | private boolean isPublic;
45 | private String createdAt;
46 | private String updatedAt;
47 |
48 | public ScopeEnum getScope() {
49 | return scope;
50 | }
51 |
52 | public void setScope(ScopeEnum scope) {
53 | this.scope = scope;
54 | }
55 |
56 | public boolean isVisible() {
57 | return visible;
58 | }
59 |
60 | public void setVisible(boolean visible) {
61 | this.visible = visible;
62 | }
63 |
64 | public String getId() {
65 | return id;
66 | }
67 |
68 | public void setId(String id) {
69 | this.id = id;
70 | }
71 |
72 | public String getTitle() {
73 | return title;
74 | }
75 |
76 | public void setTitle(String title) {
77 | this.title = title;
78 | }
79 |
80 | public String getHtmlUrl() {
81 | return htmlUrl;
82 | }
83 |
84 | public void setHtmlUrl(String htmlUrl) {
85 | this.htmlUrl = htmlUrl;
86 | }
87 |
88 | public String getDescription() {
89 | return description;
90 | }
91 |
92 | public void setDescription(String description) {
93 | this.description = description;
94 | }
95 |
96 | public Integer getFilesCount() {
97 | return filesCount;
98 | }
99 |
100 | public void setFilesCount(Integer filesCount) {
101 | this.filesCount = filesCount;
102 | }
103 |
104 | public List getFiles() {
105 | return files;
106 | }
107 |
108 | public void setFiles(List files) {
109 | this.files = files;
110 | }
111 |
112 | public List getTags() {
113 | return tags;
114 | }
115 |
116 | public void setTags(List tags) {
117 | this.tags = tags;
118 | }
119 |
120 | public GHUser getOwner() {
121 | return owner;
122 | }
123 |
124 | public void setOwner(GHUser owner) {
125 | this.owner = owner;
126 | }
127 |
128 | public boolean isPublic() {
129 | return isPublic;
130 | }
131 |
132 | public void setPublic(boolean aPublic) {
133 | isPublic = aPublic;
134 | }
135 |
136 | public String getCreatedAt() {
137 | return createdAt;
138 | }
139 |
140 | public void setCreatedAt(String createdAt) {
141 | this.createdAt = createdAt;
142 | }
143 |
144 | public String getUpdatedAt() {
145 | return updatedAt;
146 | }
147 |
148 | public void setUpdatedAt(String updatedAt) {
149 | this.updatedAt = updatedAt;
150 | }
151 |
152 | @NotNull
153 | @Override
154 | public SimpleNode[] getChildren() {
155 | return this.files.toArray(NO_CHILDREN);
156 | }
157 |
158 | private Icon findAvatar(String url) {
159 | return avatarCache.computeIfAbsent(url, (k) -> {
160 | try {
161 | return new ImageIcon(new URL(k));
162 | } catch (MalformedURLException e) {
163 | return null;
164 | }
165 | });
166 | }
167 |
168 | @NotNull
169 | protected PresentationData createPresentation() {
170 | PresentationData presentation = new PresentationData();
171 | render(presentation);
172 | return presentation;
173 | }
174 |
175 | @Override
176 | protected void update(PresentationData presentation) {
177 | render(presentation);
178 | }
179 |
180 | private void render(PresentationData presentation) {
181 | if (ScopeEnum.OWN.equals(scope)) {
182 | presentation.setIcon(isPublic ? publicIcon : secretIcon);
183 | } else {
184 | // refer to github plugin to lazy load avatar icon
185 | presentation.setIcon(isPublic ? publicIcon : secretIcon);
186 | }
187 |
188 | // Text format: tags TITLE Description n files
189 | if (tags != null) {
190 | for (String tag : tags) {
191 | presentation.addText(tag, SimpleTextAttributes.LINK_BOLD_ATTRIBUTES);
192 | presentation.addText(" ", SimpleTextAttributes.REGULAR_ATTRIBUTES);
193 | }
194 | }
195 |
196 | if (title != null) {
197 | presentation.addText(title, SimpleTextAttributes.REGULAR_BOLD_ATTRIBUTES);
198 | presentation.addText(" ", SimpleTextAttributes.REGULAR_ATTRIBUTES);
199 | }
200 |
201 | presentation.addText(description, SimpleTextAttributes.REGULAR_ATTRIBUTES);
202 | String cntTxt = " " + filesCount + " file" + (filesCount > 1 ? "s" : "");
203 | presentation.addText(cntTxt, SimpleTextAttributes.GRAYED_SMALL_ATTRIBUTES);
204 |
205 | // tooltip
206 | String activeAt = updatedAt != null ? updatedAt : createdAt;
207 | String tooltip = String.format("Last active at %s by %s", activeAt, owner.getLogin());
208 | presentation.setTooltip(tooltip);
209 | }
210 |
211 | static SnippetNodeDTO of(GHGist dto, ScopeEnum scope) {
212 | SnippetNodeDTO node = new SnippetNodeDTO();
213 | node.setScope(scope);
214 | node.setId(dto.getGistId());
215 | node.setHtmlUrl(dto.getHtmlUrl().toString());
216 | try {
217 | node.setCreatedAt(dto.getCreatedAt().toString());
218 | node.setUpdatedAt(dto.getUpdatedAt().toString());
219 | node.setOwner(dto.getOwner());
220 | } catch (IOException e) {
221 | // NOOP
222 | }
223 | node.setPublic(dto.isPublic());
224 | node.setFilesCount(dto.getFiles().size());
225 | List files = new ArrayList<>();
226 | for (GHGistFile gistFile : dto.getFiles().values()) {
227 | files.add(new FileNodeDTO(gistFile));
228 | }
229 | node.setFiles(files);
230 |
231 | parseDescription(dto, node);
232 |
233 | return node;
234 | }
235 |
236 | private static void parseDescription(GHGist dto, SnippetNodeDTO node) {
237 | node.setTitle(null);
238 | node.setTags(null);
239 | if (dto.getDescription() == null || dto.getDescription().isEmpty()) {
240 | // set description as first file name if empty
241 | for (GHGistFile fileDTO : dto.getFiles().values()) {
242 | node.setDescription(fileDTO.getFileName());
243 | break;
244 | }
245 | } else {
246 | // resolve description
247 | String txt = dto.getDescription();
248 |
249 | Matcher titleMatcher = TITLE_PATTERN.matcher(txt);
250 | if (titleMatcher.find()) {
251 | node.setTitle(titleMatcher.group(1));
252 | txt = titleMatcher.replaceFirst("");
253 | }
254 |
255 | List tags = new ArrayList<>();
256 | Matcher tagMatcher = TAG_PATTERN.matcher(txt);
257 | while (tagMatcher.find()) {
258 | tags.add(tagMatcher.group(1));
259 | }
260 | if (tags.size() > 0) {
261 | node.setTags(tags);
262 | txt = tagMatcher.replaceAll("");
263 | }
264 |
265 | node.setDescription(txt.trim());
266 | }
267 | }
268 |
269 | public boolean update(GHGist dto) {
270 | boolean updated = false;
271 | try {
272 | if (!Objects.equals(createdAt, dto.getCreatedAt()) || !Objects.equals(updatedAt, dto.getUpdatedAt())) {
273 | parseDescription(dto, this);
274 | setPublic(dto.isPublic());
275 | setCreatedAt(dto.getCreatedAt().toString());
276 | setUpdatedAt(dto.getUpdatedAt().toString());
277 |
278 | updated = true;
279 | }
280 | } catch (IOException e) {
281 | throw new RuntimeException(e);
282 | }
283 |
284 | // merge files
285 | setFilesCount(dto.getFiles().size());
286 | Set children = new HashSet<>();
287 | // traverse tree structure to remove non-existing items
288 | Iterator iterator = getFiles().iterator();
289 | while (iterator.hasNext()) {
290 | FileNodeDTO fileDTO = iterator.next();
291 | if (dto.getFiles().containsKey(fileDTO.getFilename())) {
292 | fileDTO.setContent(dto.getFiles().get(fileDTO.getFilename()).getContent());
293 | children.add(fileDTO.getFilename());
294 | } else {
295 | updated = true;
296 | iterator.remove();
297 | }
298 | }
299 |
300 | // traverse latest files to add missing items if gist changed
301 | for (GHGistFile gistFile : dto.getFiles().values()) {
302 | if (!children.contains(gistFile.getFileName())) {
303 | updated = true;
304 | getFiles().add(new FileNodeDTO(gistFile));
305 | }
306 | }
307 |
308 | return updated;
309 | }
310 | }
311 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/dto/SnippetRootNode.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.dto;
6 |
7 | import com.intellij.ui.treeStructure.SimpleNode;
8 | import org.jetbrains.annotations.NotNull;
9 | import org.kohsuke.github.GHGist;
10 |
11 | import java.util.ArrayList;
12 | import java.util.Collections;
13 | import java.util.List;
14 |
15 | public class SnippetRootNode extends SimpleNode {
16 | private List children = Collections.EMPTY_LIST;
17 |
18 | public SnippetRootNode() {
19 | }
20 |
21 | public List children() {
22 | return children;
23 | }
24 |
25 | @NotNull
26 | @Override
27 | public SimpleNode[] getChildren() {
28 | return children == null ? NO_CHILDREN : children.toArray(NO_CHILDREN);
29 | }
30 |
31 | public void resetChildren(List gistList, ScopeEnum scope) {
32 | List children = new ArrayList<>(gistList.size());
33 | for (GHGist gistDTO : gistList) {
34 | children.add(SnippetNodeDTO.of(gistDTO, scope));
35 | }
36 | this.children = children;
37 | }
38 |
39 | }
40 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/service/GistException.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.service;
6 |
7 | public class GistException extends RuntimeException {
8 | GistException(Exception e) {
9 | super(e);
10 | }
11 |
12 | public GistException(String message) {
13 | super(message);
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/service/GistSnippetService.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.service;
6 |
7 | import com.intellij.openapi.components.ServiceManager;
8 | import com.intellij.openapi.diagnostic.Logger;
9 | import com.intellij.util.containers.ContainerUtil;
10 | import org.kohsuke.github.*;
11 |
12 | import java.io.IOException;
13 | import java.util.ArrayList;
14 | import java.util.List;
15 | import java.util.Map;
16 | import java.util.concurrent.atomic.AtomicReference;
17 |
18 | /**
19 | * API Doc: https://developer.github.com/v3/gists/
20 | */
21 | public class GistSnippetService {
22 | private static final Logger logger = Logger.getInstance(GistSnippetService.class);
23 |
24 | private final GithubHelper githubHelper = new GithubHelper();
25 |
26 | // cache in memory, can be collected
27 | private Map> scopeCache = ContainerUtil.createConcurrentSoftValueMap();
28 | private Map gistCache = ContainerUtil.createConcurrentSoftValueMap();
29 |
30 | public static GistSnippetService getInstance() {
31 | return ServiceManager.getService(GistSnippetService.class);
32 | }
33 |
34 | // queryOwnGist
35 | public List queryOwnGist(String token, boolean forced) {
36 | String key = token + "#own";
37 | if (forced) {
38 | List gists = scopeCache.computeIfPresent(key, (k, v) -> scopeCache.remove(k));
39 | removeFromCache(gists);
40 | }
41 |
42 | AtomicReference> result = new AtomicReference<>();
43 | List idList = scopeCache.computeIfAbsent(key, (k) -> {
44 | try {
45 | GitHub github = githubHelper.getClient(token);
46 | PagedIterable pagedResult = github.listGists();
47 | List ghGists = pagedResult.toList();
48 | result.set(ghGists);
49 | return putIntoCache(ghGists);
50 | } catch (IOException e) {
51 | githubHelper.logError();
52 | logger.info("Failed to query own gists, error: " + e.getMessage());
53 | throw new GistException(e);
54 | }
55 | });
56 |
57 | return decideResult(token, result, idList);
58 | }
59 |
60 | private void removeFromCache(List gists) {
61 | if (gists != null) {
62 | for (String gistId : gists) {
63 | gistCache.remove(gistId);
64 | }
65 | }
66 | }
67 |
68 | private List putIntoCache(List gistList) {
69 | if (gistList != null) {
70 | List list = new ArrayList<>(gistList.size());
71 | for (GHGist gistDTO : gistList) {
72 | list.add(gistDTO.getGistId());
73 | gistCache.putIfAbsent(gistDTO.getGistId(), gistDTO);
74 | }
75 | return list;
76 | }
77 | return null;
78 | }
79 |
80 | private List decideResult(String token, AtomicReference> result, List cacheList) {
81 | // load from cache
82 | if (result.get() == null && cacheList != null) {
83 | // N + 1
84 | List gistList = new ArrayList<>(cacheList.size());
85 | for (String gistId : cacheList) {
86 | gistList.add(getGistDetail(token, gistId, false));
87 | }
88 | result.set(gistList);
89 | }
90 |
91 | return result.get();
92 | }
93 |
94 | // queryStarredGist
95 | public List queryStarredGist(String token, boolean forced) {
96 | String key = token + "#starred";
97 | if (forced) {
98 | List gists = scopeCache.computeIfPresent(key, (k, v) -> scopeCache.remove(k));
99 | removeFromCache(gists);
100 | }
101 |
102 | AtomicReference> result = new AtomicReference<>();
103 | List cacheList = scopeCache.computeIfAbsent(key, (k) -> {
104 | try {
105 | // TODO starred gists
106 | GitHub github = githubHelper.getClient(token);
107 | PagedIterable pagedResult = github.listStarredGists();
108 | List gistList = pagedResult.toList();
109 | result.set(gistList);
110 | return putIntoCache(gistList);
111 | } catch (IOException e) {
112 | logger.info("Failed to query starred gists, error: " + e.getMessage());
113 | throw new GistException(e);
114 | }
115 | });
116 |
117 | return decideResult(token, result, cacheList);
118 | }
119 |
120 | // queryPublicGist
121 | public List queryPublicGists(String keyword) {
122 | // TODO
123 | return null;
124 | }
125 |
126 | /**
127 | * @param token
128 | * @param gistId
129 | * @param forced true to load file content from remote server
130 | * @return
131 | */
132 | public GHGist getGistDetail(String token, String gistId, boolean forced) {
133 | if (forced) {
134 | gistCache.computeIfPresent(gistId, (k, v) -> gistCache.remove(k));
135 | }
136 |
137 | return gistCache.computeIfAbsent(gistId, (k) -> {
138 | try {
139 | GitHub github = githubHelper.getClient(token);
140 | return github.getGist(gistId);
141 | } catch (IOException e) {
142 | githubHelper.logError();
143 | logger.info("Failed to get gist detail, error: " + e.getMessage());
144 | throw new GistException(e);
145 | }
146 | });
147 | }
148 |
149 | public void deleteGist(String token, List gistIds) {
150 | try {
151 | for (String gistId : gistIds) {
152 | GitHub github = githubHelper.getClient(token);
153 | github.deleteGist(gistId);
154 | gistCache.remove(gistId);
155 | }
156 | String key = token + "#own";
157 | List cacheList = scopeCache.get(key);
158 | if (cacheList != null) {
159 | cacheList.removeAll(gistIds);
160 | }
161 | } catch (IOException e) {
162 | githubHelper.logError();
163 | logger.info("Failed to delete gist, error: " + e.getMessage());
164 | throw new GistException(e);
165 | }
166 | }
167 | }
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/service/GithubAccountHolder.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved.
3 | */
4 |
5 | package com.chuntung.plugin.gistsnippet.service;
6 |
7 | import com.intellij.credentialStore.CredentialAttributes;
8 | import com.intellij.ide.passwordSafe.PasswordSafe;
9 | import com.intellij.openapi.components.ServiceManager;
10 | import com.intellij.openapi.diagnostic.Logger;
11 | import com.intellij.openapi.project.Project;
12 | import org.jetbrains.plugins.github.authentication.accounts.GithubAccount;
13 |
14 | import java.lang.reflect.Field;
15 |
16 | public class GithubAccountHolder {
17 | private static final Logger logger = Logger.getInstance(GithubAccountHolder.class);
18 |
19 | private GithubAccount account;
20 | private final static Object NOT_FOUND = new Object();
21 |
22 | public static GithubAccountHolder getInstance(Project project) {
23 | return ServiceManager.getService(project, GithubAccountHolder.class);
24 | }
25 |
26 | public String getAccessToken() {
27 | String accountId = null;
28 | Object val = getProperty(account, "id");
29 | if (val == NOT_FOUND) {
30 | val = getProperty(account, "myId");
31 | }
32 | if (val != null && val != NOT_FOUND) {
33 | accountId = val.toString();
34 | }
35 |
36 | // org.jetbrains.plugins.github.authentication.accounts.GithubAccountManager.getTokenForAccount
37 | String token = PasswordSafe.getInstance().getPassword(new CredentialAttributes("IntelliJ Platform GitHub — " + accountId));
38 | if (token == null) {
39 | throw new GistException("Only token is supported to access GitHub API, please add account through token");
40 | }
41 | return token;
42 | }
43 |
44 | private Object getProperty(Object object, String property) {
45 | try {
46 | Field field = object.getClass().getDeclaredField(property);
47 | field.setAccessible(true);
48 | return field.get(object);
49 | } catch (Exception e) {
50 | return NOT_FOUND;
51 | }
52 | }
53 |
54 | public GithubAccount getAccount() {
55 | return account;
56 | }
57 |
58 | public void setAccount(GithubAccount account) {
59 | this.account = account;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/service/GithubHelper.java:
--------------------------------------------------------------------------------
1 | package com.chuntung.plugin.gistsnippet.service;
2 |
3 | import org.kohsuke.github.GitHub;
4 | import org.kohsuke.github.GitHubBuilder;
5 | import org.kohsuke.github.connector.GitHubConnector;
6 | import org.kohsuke.github.connector.GitHubConnectorRequest;
7 | import org.kohsuke.github.connector.GitHubConnectorResponse;
8 |
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.util.HashMap;
12 | import java.util.List;
13 | import java.util.Map;
14 | import java.util.concurrent.ConcurrentHashMap;
15 | import java.util.concurrent.ConcurrentMap;
16 | import java.util.concurrent.atomic.AtomicLong;
17 | import java.util.stream.Collectors;
18 |
19 | public class GithubHelper {
20 | private static final String fallbackEndpoint = "https://api-github-com.chuntung.com";
21 |
22 | private final ConcurrentMap clientCache = new ConcurrentHashMap<>();
23 | private final AtomicLong errorCount = new AtomicLong(0);
24 |
25 | public GithubHelper(){
26 | }
27 |
28 | public void logError() {
29 | errorCount.incrementAndGet();
30 | clientCache.clear();
31 | }
32 |
33 | public GitHub getClient(String token) throws IOException {
34 | GitHub gitHub = clientCache.computeIfAbsent(token, k -> {
35 | GitHubBuilder gitHubBuilder = new GitHubBuilder()
36 | .withOAuthToken(token);
37 | if (errorCount.get() >= 3) {
38 | gitHubBuilder.withEndpoint(fallbackEndpoint)
39 | .withConnector(new FallbackGitHubConnector(GitHubConnector.DEFAULT));
40 | }
41 | try {
42 | return gitHubBuilder.build();
43 | } catch (IOException e) {
44 | return null;
45 | }
46 | });
47 | if (gitHub == null) {
48 | throw new IOException("Failed to init github client");
49 | }
50 | return gitHub;
51 | }
52 |
53 | private static class FallbackGitHubConnector implements GitHubConnector {
54 | private GitHubConnector delegate;
55 |
56 | public FallbackGitHubConnector(GitHubConnector delegate) {
57 | this.delegate = delegate;
58 | }
59 |
60 | @Override
61 | public GitHubConnectorResponse send(GitHubConnectorRequest gitHubConnectorRequest) throws IOException {
62 | return new FallbackGitHubConnectorResponse(gitHubConnectorRequest, delegate.send(gitHubConnectorRequest));
63 | }
64 | }
65 |
66 | private static class FallbackGitHubConnectorResponse extends GitHubConnectorResponse {
67 | private final GitHubConnectorResponse response;
68 |
69 | protected FallbackGitHubConnectorResponse(GitHubConnectorRequest request, GitHubConnectorResponse response) {
70 | super(request, response.statusCode(), resolveLink(response.allHeaders()));
71 | this.response = response;
72 | }
73 |
74 | private static Map> resolveLink(Map> headers) {
75 | List link = headers.get("Link");
76 | if (link != null) {
77 | List resolved = link.stream()
78 | .map(x -> x.replace("https://api.github.com", fallbackEndpoint))
79 | .collect(Collectors.toList());
80 | if (!resolved.isEmpty()) {
81 | HashMap> newMap = new HashMap>(headers);
82 | newMap.put("Link", resolved);
83 | return newMap;
84 | }
85 | }
86 | return headers;
87 | }
88 |
89 | @Override
90 | public InputStream bodyStream() throws IOException {
91 | return response.bodyStream();
92 | }
93 |
94 | @Override
95 | public void close() throws IOException {
96 | response.close();
97 | }
98 | }
99 | }
--------------------------------------------------------------------------------
/src/main/java/com/chuntung/plugin/gistsnippet/view/CustomActionLink.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2000-2016 JetBrains s.r.o.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | package com.chuntung.plugin.gistsnippet.view;
17 |
18 | import com.intellij.openapi.actionSystem.ActionPlaces;
19 | import com.intellij.openapi.actionSystem.AnAction;
20 | import com.intellij.openapi.actionSystem.DataProvider;
21 | import com.intellij.openapi.actionSystem.PlatformDataKeys;
22 | import com.intellij.openapi.actionSystem.ex.ActionUtil;
23 | import com.intellij.ui.components.labels.LinkLabel;
24 | import com.intellij.ui.components.labels.LinkListener;
25 | import com.intellij.util.ui.UIUtil;
26 | import org.jetbrains.annotations.NonNls;
27 | import org.jetbrains.annotations.NotNull;
28 | import org.jetbrains.annotations.Nullable;
29 | import org.jetbrains.annotations.TestOnly;
30 |
31 | import javax.swing.*;
32 | import java.awt.*;
33 | import java.awt.event.InputEvent;
34 |
35 | /**
36 | * @author Konstantin Bulenkov
37 | */
38 | public class CustomActionLink extends LinkLabel