├── .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 implements DataProvider { 39 | private final AnAction myAction; 40 | private final String myPlace = ActionPlaces.UNKNOWN; 41 | private InputEvent myEvent; 42 | private Color myVisitedColor; 43 | private Color myActiveColor; 44 | private Color myNormalColor; 45 | 46 | public CustomActionLink(String text, @NotNull AnAction action) { 47 | this(text, null, action); 48 | } 49 | 50 | public CustomActionLink(String text, Icon icon, @NotNull AnAction action) { 51 | this(text, icon, action, null); 52 | } 53 | 54 | public CustomActionLink(String text, Icon icon, @NotNull AnAction action, @Nullable final Runnable onDone) { 55 | super(text, icon); 56 | setListener(new LinkListener() { 57 | @Override 58 | public void linkSelected(LinkLabel aSource, Object aLinkData) { 59 | ActionUtil.invokeAction(myAction, CustomActionLink.this, myPlace, myEvent, onDone); 60 | } 61 | }, null); 62 | myAction = action; 63 | } 64 | 65 | @Override 66 | public void doClick(InputEvent e) { 67 | myEvent = e; 68 | super.doClick(); 69 | } 70 | 71 | @Override 72 | protected Color getVisited() { 73 | return myVisitedColor == null ? super.getVisited() : myVisitedColor; 74 | } 75 | 76 | public Color getActiveColor() { 77 | return myActiveColor == null ? super.getActive() : myActiveColor; 78 | } 79 | 80 | @Override 81 | protected Color getTextColor() { 82 | return myUnderline ? getActiveColor() : getNormal(); 83 | } 84 | 85 | @Override 86 | protected Color getNormal() { 87 | return myNormalColor == null ? super.getNormal() : myNormalColor; 88 | } 89 | 90 | public void setVisitedColor(Color visitedColor) { 91 | myVisitedColor = visitedColor; 92 | } 93 | 94 | public void setActiveColor(Color activeColor) { 95 | myActiveColor = activeColor; 96 | } 97 | 98 | public void setNormalColor(Color normalColor) { 99 | myNormalColor = normalColor; 100 | } 101 | 102 | @Override 103 | public Object getData(@NotNull @NonNls String dataId) { 104 | if (PlatformDataKeys.DOMINANT_HINT_AREA_RECTANGLE.is(dataId)) { 105 | final Point p = SwingUtilities.getRoot(this).getLocationOnScreen(); 106 | return new Rectangle(p.x, p.y + getHeight(), 0, 0); 107 | } 108 | if (PlatformDataKeys.CONTEXT_MENU_POINT.is(dataId)) { 109 | return SwingUtilities.convertPoint(this, 0, getHeight(), UIUtil.getRootPane(this)); 110 | } 111 | return myAction instanceof DataProvider ? ((DataProvider)myAction).getData(dataId) : null; 112 | } 113 | 114 | @TestOnly 115 | public AnAction getAction() { 116 | return myAction; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/chuntung/plugin/gistsnippet/view/CustomDropDownLink.java: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. 2 | package com.chuntung.plugin.gistsnippet.view; 3 | 4 | import com.intellij.icons.AllIcons; 5 | import com.intellij.openapi.ui.popup.IPopupChooserBuilder; 6 | import com.intellij.openapi.ui.popup.JBPopup; 7 | import com.intellij.openapi.ui.popup.JBPopupFactory; 8 | import com.intellij.ui.awt.RelativePoint; 9 | import com.intellij.ui.components.labels.LinkLabel; 10 | import com.intellij.util.Consumer; 11 | import com.intellij.util.containers.Convertor; 12 | import com.intellij.util.ui.JBUI; 13 | import org.jetbrains.annotations.NotNull; 14 | import org.jetbrains.annotations.Nullable; 15 | 16 | import javax.swing.*; 17 | import javax.swing.plaf.metal.MetalLabelUI; 18 | import java.awt.*; 19 | import java.util.List; 20 | 21 | public class CustomDropDownLink extends LinkLabel { 22 | private T chosenItem; 23 | 24 | public CustomDropDownLink(@NotNull T value, @NotNull Runnable clickAction) { 25 | super(value.toString(), AllIcons.General.LinkDropTriangle, (s, d) -> clickAction.run()); 26 | chosenItem = value; 27 | init(); 28 | } 29 | 30 | public CustomDropDownLink(@NotNull T value, @NotNull Convertor popupBuilder) { 31 | super(value.toString(), AllIcons.General.LinkDropTriangle); 32 | chosenItem = value; 33 | 34 | setListener((linkLabel, d) -> { 35 | JBPopup popup = popupBuilder.convert((CustomDropDownLink)linkLabel); 36 | Point showPoint = new Point(0, getHeight() + JBUI.scale(4)); 37 | popup.show(new RelativePoint(this, showPoint)); 38 | }, null); 39 | 40 | init(); 41 | } 42 | 43 | public CustomDropDownLink(@NotNull T initialItem, @NotNull List items, @Nullable Consumer itemChosenAction, boolean updateLabel) { 44 | this(initialItem, (linkLabel) -> { 45 | IPopupChooserBuilder popupBuilder = JBPopupFactory.getInstance().createPopupChooserBuilder(items). 46 | setRenderer(new LinkCellRenderer<>(linkLabel)). 47 | setItemChosenCallback(t -> { 48 | linkLabel.chosenItem = t; 49 | if (updateLabel) { 50 | linkLabel.setText(t.toString()); 51 | } 52 | 53 | if (itemChosenAction != null) { 54 | itemChosenAction.consume(t); 55 | } 56 | }); 57 | return popupBuilder.createPopup(); 58 | }); 59 | } 60 | 61 | private void init() { 62 | setIconTextGap(JBUI.scale(1)); 63 | setHorizontalAlignment(SwingConstants.LEADING); 64 | setHorizontalTextPosition(SwingConstants.LEADING); 65 | 66 | setUI(new MetalLabelUI() { 67 | @Override 68 | protected String layoutCL(JLabel label, FontMetrics fontMetrics, String text, Icon icon, 69 | Rectangle viewR, Rectangle iconR, Rectangle textR) { 70 | String result = super.layoutCL(label, fontMetrics, text, icon, viewR, iconR, textR); 71 | iconR.y += JBUI.scale(1); 72 | return result; 73 | } 74 | }); 75 | } 76 | 77 | public T getChosenItem() { 78 | return chosenItem; 79 | } 80 | 81 | private static class LinkCellRenderer extends JLabel implements ListCellRenderer { 82 | private final JComponent owner; 83 | 84 | private LinkCellRenderer(JComponent owner) { 85 | this.owner = owner; 86 | setBorder(JBUI.Borders.empty(0, 5, 0, 10)); 87 | } 88 | 89 | @Override 90 | public Dimension getPreferredSize() { 91 | return recomputeSize(super.getPreferredSize()); 92 | } 93 | 94 | @Override 95 | public Dimension getMinimumSize() { 96 | return recomputeSize(super.getMinimumSize()); 97 | } 98 | 99 | private Dimension recomputeSize(@NotNull Dimension size) { 100 | size.height = Math.max(size.height, JBUI.scale(22)); 101 | size.width = Math.max(size.width, owner.getPreferredSize().width); 102 | return size; 103 | } 104 | 105 | @Override 106 | public Component getListCellRendererComponent(JList list, T value, int index, boolean isSelected, boolean cellHasFocus) { 107 | setText(value.toString()); 108 | setEnabled(list.isEnabled()); 109 | setOpaque(true); 110 | 111 | setBackground(isSelected ? list.getSelectionBackground() : UIManager.getColor("Label.background")); 112 | setForeground(isSelected ? list.getSelectionForeground() : list.getForeground()); 113 | 114 | return this; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/main/java/com/chuntung/plugin/gistsnippet/view/CustomTreeStructure.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved. 3 | */ 4 | 5 | package com.chuntung.plugin.gistsnippet.view; 6 | 7 | import com.chuntung.plugin.gistsnippet.dto.SnippetNodeDTO; 8 | import com.intellij.ui.treeStructure.SimpleTreeStructure; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | /** 12 | * Custom tree structure to support filtering. 13 | */ 14 | public class CustomTreeStructure extends SimpleTreeStructure { 15 | private Object root; 16 | 17 | CustomTreeStructure(Object root) { 18 | this.root = root; 19 | } 20 | 21 | @NotNull 22 | @Override 23 | public Object getRootElement() { 24 | return root; 25 | } 26 | 27 | // use this to filter out invisible item 28 | public boolean isValid(@NotNull Object element) { 29 | if (element instanceof SnippetNodeDTO) { 30 | return ((SnippetNodeDTO) element).isVisible(); 31 | } 32 | return true; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/com/chuntung/plugin/gistsnippet/view/InsertGistDialog.form: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
129 | -------------------------------------------------------------------------------- /src/main/java/com/chuntung/plugin/gistsnippet/view/InsertGistDialog.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Chuntung Ho. Some rights reserved. 3 | */ 4 | 5 | package com.chuntung.plugin.gistsnippet.view; 6 | 7 | import com.chuntung.plugin.gistsnippet.action.CustomComboBoxAction; 8 | import com.chuntung.plugin.gistsnippet.action.DeleteAction; 9 | import com.chuntung.plugin.gistsnippet.action.OpenInBrowserAction; 10 | import com.chuntung.plugin.gistsnippet.action.ReloadAction; 11 | import com.chuntung.plugin.gistsnippet.dto.FileNodeDTO; 12 | import com.chuntung.plugin.gistsnippet.dto.ScopeEnum; 13 | import com.chuntung.plugin.gistsnippet.dto.SnippetNodeDTO; 14 | import com.chuntung.plugin.gistsnippet.dto.SnippetRootNode; 15 | import com.chuntung.plugin.gistsnippet.service.GistException; 16 | import com.chuntung.plugin.gistsnippet.service.GistSnippetService; 17 | import com.chuntung.plugin.gistsnippet.service.GithubAccountHolder; 18 | import com.intellij.icons.AllIcons; 19 | import com.intellij.ide.BrowserUtil; 20 | import com.intellij.ide.CommonActionsManager; 21 | import com.intellij.ide.DefaultTreeExpander; 22 | import com.intellij.ide.TreeExpander; 23 | import com.intellij.ide.util.PropertiesComponent; 24 | import com.intellij.ide.util.treeView.AbstractTreeStructure; 25 | import com.intellij.notification.*; 26 | import com.intellij.openapi.Disposable; 27 | import com.intellij.openapi.actionSystem.*; 28 | import com.intellij.openapi.application.ApplicationManager; 29 | import com.intellij.openapi.application.ModalityState; 30 | import com.intellij.openapi.editor.Editor; 31 | import com.intellij.openapi.editor.EditorFactory; 32 | import com.intellij.openapi.editor.LogicalPosition; 33 | import com.intellij.openapi.editor.ex.EditorEx; 34 | import com.intellij.openapi.editor.highlighter.EditorHighlighterFactory; 35 | import com.intellij.openapi.fileTypes.FileType; 36 | import com.intellij.openapi.progress.ProgressIndicator; 37 | import com.intellij.openapi.progress.Task; 38 | import com.intellij.openapi.project.DumbAwareAction; 39 | import com.intellij.openapi.project.Project; 40 | import com.intellij.openapi.ui.DialogWrapper; 41 | import com.intellij.openapi.util.IconLoader; 42 | import com.intellij.ui.ScrollPaneFactory; 43 | import com.intellij.ui.components.JBTabbedPane; 44 | import com.intellij.ui.components.labels.LinkLabel; 45 | import com.intellij.ui.tree.AsyncTreeModel; 46 | import com.intellij.ui.tree.StructureTreeModel; 47 | import com.intellij.ui.treeStructure.SimpleTree; 48 | import com.intellij.util.ui.UIUtil; 49 | import com.intellij.util.ui.tree.TreeModelAdapter; 50 | import org.jetbrains.annotations.NotNull; 51 | import org.jetbrains.annotations.Nullable; 52 | import org.jetbrains.plugins.github.authentication.GithubAuthenticationManager; 53 | import org.jetbrains.plugins.github.authentication.accounts.GithubAccount; 54 | import org.kohsuke.github.GHGist; 55 | 56 | import javax.swing.*; 57 | import javax.swing.event.TreeModelEvent; 58 | import javax.swing.event.TreeSelectionEvent; 59 | import javax.swing.tree.DefaultMutableTreeNode; 60 | import javax.swing.tree.TreePath; 61 | import java.awt.*; 62 | import java.lang.reflect.Constructor; 63 | import java.util.ArrayList; 64 | import java.util.List; 65 | import java.util.Objects; 66 | import java.util.Set; 67 | 68 | public class InsertGistDialog extends DialogWrapper { 69 | public static final String SPLIT_LEFT_WIDTH = "InsertGistDialog.yoursSplitLeftWidth"; 70 | private static final NotificationGroup notificationGroup = 71 | new NotificationGroup("GistSnippet.NotificationGroup", NotificationDisplayType.BALLOON, true);; 72 | 73 | private JPanel mainPanel; 74 | private JBTabbedPane tabbedPane; 75 | private LinkLabel yoursTabTitle; 76 | private JSplitPane yoursSplitPane; 77 | private JTree snippetTree; 78 | private Editor editor; 79 | private TreeExpander myTreeExpander; 80 | 81 | private JButton searchButton; 82 | private JTextField textField1; 83 | private JComboBox languageComboBox; 84 | private JComboBox sortByComboBox; 85 | private JScrollPane scrollPane; 86 | 87 | private Project project; 88 | private final boolean insertable; 89 | private GistSnippetService service; 90 | 91 | private SnippetRootNode snippetRoot; 92 | private StructureTreeModel snippetStructure; 93 | private CustomComboBoxAction scopeAction; 94 | private CustomComboBoxAction typeAction; 95 | Icon ownIcon = IconLoader.getIcon("/images/own.png", InsertGistDialog.class); 96 | Icon starredIcon = IconLoader.getIcon("/images/starred.png", InsertGistDialog.class); 97 | 98 | // remember last preview file 99 | private volatile String showingFileUrl; 100 | private volatile String editorFileUrl; 101 | 102 | // to be returned 103 | private String selectedText; 104 | 105 | // account holder in project level 106 | private GithubAccountHolder accountHolder; 107 | 108 | public InsertGistDialog(Project project, boolean insertable) { 109 | super(project); 110 | this.project = project; 111 | this.insertable = insertable; 112 | this.service = GistSnippetService.getInstance(); 113 | accountHolder = GithubAccountHolder.getInstance(project); 114 | init(project); 115 | } 116 | 117 | // create custom components before ui-designer init 118 | private void createUIComponents() { 119 | // Replace JTree with SimpleTree here due to ui designer fails to preview SimpleTree. 120 | snippetTree = new SimpleTree(); 121 | snippetTree.addTreeSelectionListener(e -> onSelect(e)); 122 | 123 | scrollPane = ScrollPaneFactory.createScrollPane(snippetTree, true); 124 | 125 | // use tree structure for rendering 126 | snippetRoot = new SnippetRootNode(); 127 | // fix API removal issue 128 | try { 129 | // after 192 130 | Constructor constructor = StructureTreeModel.class.getConstructor(AbstractTreeStructure.class, Disposable.class); 131 | snippetStructure = constructor.newInstance(new CustomTreeStructure(snippetRoot), myDisposable); 132 | } catch (NoSuchMethodException e) { 133 | try { 134 | // before 192 135 | Constructor constructor = StructureTreeModel.class.getConstructor(AbstractTreeStructure.class); 136 | snippetStructure = constructor.newInstance(new CustomTreeStructure(snippetRoot)); 137 | } catch (ReflectiveOperationException ex) { 138 | // NOOP 139 | } 140 | } catch (ReflectiveOperationException e) { 141 | // NOOP 142 | } 143 | if (snippetStructure != null) { 144 | AsyncTreeModel treeModel = new AsyncTreeModel(snippetStructure, myDisposable); 145 | 146 | // make it focusable after rendering 147 | treeModel.addTreeModelListener(new TreeModelAdapter() { 148 | protected void process(@NotNull TreeModelEvent event, @NotNull EventType type) { 149 | if (snippetRoot.children().size() > 0 && snippetTree.getSelectionCount() == 0) { 150 | UIUtil.invokeLaterIfNeeded(() -> { 151 | if (snippetRoot.children().size() > 0 && snippetTree.getSelectionCount() == 0) { 152 | snippetTree.setSelectionRow(0); 153 | snippetTree.requestFocusInWindow(); 154 | } 155 | }); 156 | } 157 | } 158 | }); 159 | 160 | snippetTree.setModel(treeModel); 161 | } 162 | myTreeExpander = new DefaultTreeExpander(snippetTree); 163 | 164 | // init empty editor 165 | EditorFactory editorFactory = EditorFactory.getInstance(); 166 | editor = editorFactory.createViewer(editorFactory.createDocument(""), project); 167 | } 168 | 169 | private void filterByPublic(Boolean isPublic) { 170 | for (SnippetNodeDTO child : snippetRoot.children()) { 171 | if (isPublic == null) { 172 | child.setVisible(true); 173 | } else { 174 | child.setVisible(isPublic.equals(child.isPublic())); 175 | } 176 | } 177 | 178 | snippetStructure.invalidate(); 179 | } 180 | 181 | // init after ui-designer setup 182 | private void init(Project project) { 183 | setTitle("Insert Gist"); 184 | setOKButtonText("Insert"); 185 | if (!insertable) { 186 | setOKButtonTooltip("Document is read only, snippet could not be inserted."); 187 | } 188 | // to be enabled after file loaded 189 | setOKActionEnabled(false); 190 | // focus cancel button by default 191 | getCancelAction().putValue(FOCUSED_ACTION, true); 192 | 193 | // after project field assigned 194 | ((SimpleTree) snippetTree).setPopupGroup(getPopupActions(), ActionPlaces.UNKNOWN); 195 | 196 | GithubAuthenticationManager authenticationManager = GithubAuthenticationManager.getInstance(); 197 | Set accounts = authenticationManager.getAccounts(); 198 | if (accounts != null && accounts.size() > 0) { 199 | initYoursPane(new ArrayList<>(accounts)); 200 | } else { 201 | // hide yours tab content without github account 202 | yoursSplitPane.setVisible(false); 203 | yoursTabTitle = new CustomActionLink("Add GitHub Account", new AnAction() { 204 | @Override 205 | public void actionPerformed(@NotNull AnActionEvent e) { 206 | boolean existing = authenticationManager.ensureHasAccounts(project); 207 | if (existing) { 208 | List accountList = new ArrayList<>(authenticationManager.getAccounts()); 209 | initYoursPane(accountList); 210 | } 211 | } 212 | }); 213 | tabbedPane.setTabComponentAt(0, yoursTabTitle); 214 | } 215 | 216 | // TODO discover feature is not done 217 | tabbedPane.remove(1); 218 | 219 | super.init(); 220 | } 221 | 222 | 223 | // tree popup actions 224 | private ActionGroup getPopupActions() { 225 | DefaultActionGroup group = new DefaultActionGroup(); 226 | 227 | // delete gist 228 | group.add(new DeleteAction(snippetTree, snippetStructure, snippetRoot, project)); 229 | 230 | // reload gist file 231 | group.add(new ReloadAction(snippetTree, e -> { 232 | DefaultMutableTreeNode node = (DefaultMutableTreeNode) snippetTree.getLastSelectedPathComponent(); 233 | loadFileContent(project, node, true); 234 | })); 235 | 236 | // open in browser for gist or file 237 | group.add(new OpenInBrowserAction(snippetTree)); 238 | 239 | return group; 240 | } 241 | 242 | // tree toolbar actions 243 | private ActionGroup getToolbarActions() { 244 | DefaultActionGroup group = new DefaultActionGroup(); 245 | 246 | // scope combo-box 247 | group.add(scopeAction = CustomComboBoxAction.create( 248 | new AnAction("Own", "Load own gists", ownIcon) { 249 | @Override 250 | public void actionPerformed(@NotNull AnActionEvent e) { 251 | boolean forced = "Own".equals(scopeAction.getText()); 252 | loadOwnGist(forced); 253 | } 254 | }, 255 | 256 | new AnAction("Starred", "Load starred gists", starredIcon) { 257 | @Override 258 | public void actionPerformed(@NotNull AnActionEvent e) { 259 | boolean forced = "Starred".equals(scopeAction.getText()); 260 | loadStarredGist(forced); 261 | } 262 | }) 263 | ); 264 | 265 | // type combo-box 266 | group.add( 267 | typeAction = CustomComboBoxAction.create( 268 | DumbAwareAction.create("-Type-", e -> filterByPublic(null)) 269 | , DumbAwareAction.create("Public", e -> filterByPublic(true)) 270 | , DumbAwareAction.create("Secret", e -> filterByPublic(false)) 271 | ) 272 | ); 273 | 274 | // refresh own/starred 275 | group.add(new AnAction("Refresh", "Refresh gist list", AllIcons.Actions.Refresh) { 276 | @Override 277 | public void actionPerformed(@NotNull AnActionEvent e) { 278 | if ("Own".equals(scopeAction.getText())) { 279 | loadOwnGist(true); 280 | } else { 281 | loadStarredGist(true); 282 | } 283 | } 284 | }); 285 | 286 | group.addSeparator(); 287 | 288 | 289 | group.add(CommonActionsManager.getInstance().createExpandAllAction(myTreeExpander, mainPanel)); 290 | group.add(CommonActionsManager.getInstance().createCollapseAllAction(myTreeExpander, mainPanel)); 291 | 292 | return group; 293 | } 294 | 295 | private void initYoursPane(List accountList) { 296 | yoursSplitPane.setVisible(true); 297 | 298 | // init toolbar 299 | ActionToolbar actionToolbar = ActionManager.getInstance() 300 | .createActionToolbar("Gist.toolbar", getToolbarActions(), true); 301 | actionToolbar.setTargetComponent(mainPanel); 302 | ((JPanel) yoursSplitPane.getLeftComponent()).add(actionToolbar.getComponent(), BorderLayout.NORTH); 303 | 304 | // load remembered width 305 | int width = PropertiesComponent.getInstance().getInt(SPLIT_LEFT_WIDTH, 240); 306 | yoursSplitPane.getLeftComponent().setPreferredSize(new Dimension(width, -1)); 307 | 308 | // bind editor 309 | yoursSplitPane.setRightComponent(editor.getComponent()); 310 | 311 | // display account name on 1st tab 312 | if (accountHolder.getAccount() == null) { 313 | accountHolder.setAccount(accountList.get(0)); 314 | } 315 | 316 | yoursTabTitle = new CustomDropDownLink<>(accountHolder.getAccount(), accountList, chosenItem -> { 317 | if (!chosenItem.equals(accountHolder.getAccount())) { 318 | accountHolder.setAccount(chosenItem); 319 | 320 | // load All Own Gist by default 321 | scopeAction.reset(); 322 | loadOwnGist(false); 323 | } 324 | }, true); 325 | 326 | tabbedPane.setTabComponentAt(0, yoursTabTitle); 327 | 328 | loadOwnGist(false); 329 | } 330 | 331 | private void onSelect(TreeSelectionEvent e) { 332 | TreePath treePath = e.getNewLeadSelectionPath(); 333 | if (treePath == null || treePath.getLastPathComponent() == null) { 334 | return; 335 | } 336 | DefaultMutableTreeNode selected = (DefaultMutableTreeNode) treePath.getLastPathComponent(); 337 | // show the first file when gist item is selected 338 | if (selected.getUserObject() instanceof SnippetNodeDTO && selected.getChildCount() > 0) { 339 | selected = (DefaultMutableTreeNode) selected.getFirstChild(); 340 | } 341 | if (selected.getUserObject() instanceof FileNodeDTO) { 342 | // show file content 343 | FileNodeDTO gistFileDTO = getUserObject(selected); 344 | showingFileUrl = gistFileDTO.getRawUrl(); 345 | 346 | // just show cache in view 347 | if (gistFileDTO.getContent() != null) { 348 | showInEditor(gistFileDTO, false); 349 | } else { 350 | // load forcibly 351 | loadFileContent(project, selected, true); 352 | } 353 | } 354 | } 355 | 356 | // run in dispatch thread 357 | private void showInEditor(FileNodeDTO fileDTO, boolean forced) { 358 | if (!Objects.equals(editorFileUrl, fileDTO.getRawUrl()) || forced) { 359 | // setText require write access 360 | ApplicationManager.getApplication().runWriteAction(() -> { 361 | if (editor.isDisposed()) { 362 | return; 363 | } 364 | 365 | EditorHighlighterFactory highlighterFactory = EditorHighlighterFactory.getInstance(); 366 | FileType fileType = FileNodeDTO.getFileType(fileDTO); 367 | editor.getDocument().setText(fileDTO.getContent()); 368 | ((EditorEx) editor).setHighlighter(highlighterFactory.createEditorHighlighter(project, fileType)); 369 | editorFileUrl = fileDTO.getRawUrl(); 370 | 371 | // make it focusable 372 | editor.getCaretModel().moveToLogicalPosition(new LogicalPosition(0, 0)); 373 | editor.getContentComponent().requestFocusInWindow(); 374 | snippetTree.requestFocusInWindow(); 375 | 376 | setOKActionEnabled(insertable); 377 | }); 378 | } 379 | } 380 | 381 | // load file content 382 | private void loadFileContent(Project project, DefaultMutableTreeNode selected, boolean forced) { 383 | DefaultMutableTreeNode parent = (DefaultMutableTreeNode) selected.getParent(); 384 | SnippetNodeDTO snippet = getUserObject(parent); 385 | new Task.Backgroundable(project, "Loading gist files...") { 386 | boolean shouldUpdate = false; 387 | 388 | @Override 389 | public void run(@NotNull ProgressIndicator indicator) { 390 | GHGist gist = service.getGistDetail(accountHolder.getAccessToken(), snippet.getId(), forced); 391 | shouldUpdate = snippet.update(gist); 392 | } 393 | 394 | @Override 395 | public void onThrowable(Throwable e) { 396 | notifyWarn("Failed to get Gist files, " + e.getMessage()); 397 | } 398 | 399 | @Override 400 | // this will run in dispatch thread 401 | public void onSuccess() { 402 | if (shouldUpdate) { 403 | // invalidating is in a new background task, should invoker later in dispatch thread 404 | snippetStructure.invalidate(new TreePath(parent), true).onSuccess((treePath) -> { 405 | ApplicationManager.getApplication().invokeLater(this::previewFile, ModalityState.stateForComponent(mainPanel)); 406 | }); 407 | } else { 408 | previewFile(); 409 | } 410 | } 411 | 412 | private void previewFile() { 413 | // selected may be changed by user, check before replacing editor content 414 | FileNodeDTO gistFileDTO = getUserObject(selected); 415 | if (Objects.equals(showingFileUrl, gistFileDTO.getRawUrl())) { 416 | if (gistFileDTO.getContent() != null) { 417 | showInEditor(gistFileDTO, forced); 418 | } 419 | } 420 | } 421 | }.queue(); 422 | } 423 | 424 | private T getUserObject(DefaultMutableTreeNode node) { 425 | return (T) (node.getUserObject()); 426 | } 427 | 428 | private void loadOwnGist(boolean forced) { 429 | // reset type filter for switch 430 | if (!forced) { 431 | typeAction.reset(); 432 | } 433 | 434 | // com.intellij.util.io.HttpRequests#process does not allow Network accessed in dispatch thread or read action 435 | // start a background task to bypass api limitation 436 | new Task.Backgroundable(project, "Loading own gists...") { 437 | @Override 438 | public void run(@NotNull ProgressIndicator indicator) { 439 | try { 440 | List ownGist = service.queryOwnGist(accountHolder.getAccessToken(), forced); 441 | // non-modal task should not invoke onSuccess() in modal dialog initialization. 442 | // it will be blocked in dispatch thread by modal dialog, here just run in background 443 | renderTree(ownGist, ScopeEnum.OWN); 444 | } catch (GistException e) { 445 | notifyWarn("Failed to load own gists, error: " + e.getMessage()); 446 | } 447 | } 448 | }.queue(); 449 | } 450 | 451 | private void loadStarredGist(boolean forced) { 452 | // reset type filter 453 | typeAction.reset(); 454 | 455 | new Task.Backgroundable(project, "Loading starred gists...") { 456 | @Override 457 | public void run(@NotNull ProgressIndicator indicator) { 458 | try { 459 | List starredGist = service.queryStarredGist(accountHolder.getAccessToken(), forced); 460 | renderTree(starredGist, ScopeEnum.STARRED); 461 | } catch (GistException e) { 462 | notifyWarn("Failed to load starred gists, error: " + e.getMessage()); 463 | } 464 | } 465 | }.queue(); 466 | } 467 | 468 | private void renderTree(List gistList, ScopeEnum scope) { 469 | snippetRoot.resetChildren(gistList, scope); 470 | 471 | // filter by type if selected 472 | if ("Public".equals(typeAction.getText())) { 473 | filterByPublic(true); 474 | } else if ("Secret".equals(typeAction.getText())) { 475 | filterByPublic(false); 476 | } else { 477 | snippetStructure.invalidate(); 478 | } 479 | } 480 | 481 | @Nullable 482 | @Override 483 | protected JComponent createCenterPanel() { 484 | return mainPanel; 485 | } 486 | 487 | @Override // remember window position and size 488 | protected String getDimensionServiceKey() { 489 | return "GistSnippet.InsertGistDialog"; 490 | } 491 | 492 | @Override 493 | protected String getHelpId() { 494 | return "https://gist.chuntung.com"; 495 | } 496 | 497 | @Override 498 | protected void doHelpAction() { 499 | if (myHelpAction.isEnabled()) { 500 | BrowserUtil.browse(getHelpId()); 501 | } 502 | } 503 | 504 | @Override 505 | protected void doOKAction() { 506 | if (getOKAction().isEnabled()) { 507 | // set selected text for external action usage 508 | selectedText = editor.getSelectionModel().getSelectedText(); 509 | if (selectedText == null) { 510 | selectedText = editor.getDocument().getText(); 511 | } 512 | } 513 | 514 | super.doOKAction(); 515 | } 516 | 517 | @Override 518 | protected void dispose() { 519 | if (editor != null && !editor.isDisposed()) { 520 | EditorFactory.getInstance().releaseEditor(editor); 521 | } 522 | 523 | // remember left width 524 | PropertiesComponent.getInstance().setValue(SPLIT_LEFT_WIDTH, yoursSplitPane.getLeftComponent().getWidth(), 220); 525 | super.dispose(); 526 | } 527 | 528 | public void notifyWarn(String warn) { 529 | Notification notification = notificationGroup.createNotification(warn, NotificationType.WARNING); 530 | Notifications.Bus.notify(notification, project); 531 | } 532 | 533 | /** 534 | * @return selected text or whole file content. 535 | */ 536 | public String getSelectedText() { 537 | return selectedText; 538 | } 539 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | com.chuntung.plugin.gistsnippet 7 | Gist Snippet 8 | Chuntung Ho 9 | 10 | Getting Started 12 |

13 | A code snippet tool based on GitHub Gist, that provides with a feature to fetch own or starred gists of GitHub accounts. 14 | It depends on built-in GitHub plugin which should be enabled. 15 | ]]>
16 | 17 | 19 | com.intellij.modules.lang 20 | org.jetbrains.plugins.github 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
-------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | background 5 | 6 | 7 | 8 | Layer 1 9 | 12 | 14 | 16 | / 19 | 20 | -------------------------------------------------------------------------------- /src/main/resources/images/gist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/src/main/resources/images/gist.png -------------------------------------------------------------------------------- /src/main/resources/images/own.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/src/main/resources/images/own.png -------------------------------------------------------------------------------- /src/main/resources/images/public.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/src/main/resources/images/public.png -------------------------------------------------------------------------------- /src/main/resources/images/secret.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/src/main/resources/images/secret.png -------------------------------------------------------------------------------- /src/main/resources/images/starred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/src/main/resources/images/starred.png -------------------------------------------------------------------------------- /src/main/resources/images/unstarred.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chuntungho/gist-snippet/91bf46f197759c78be90ed243ba5ae999a088162/src/main/resources/images/unstarred.png --------------------------------------------------------------------------------