PyCharmでdocstringを書くのが面倒すぎたのでplugin作った

適当なpluginとかそもそもデフォルト機能であるはず。
と思ったのに見当たらなかったので。

## 結論

### サンプルをちょっといじって使う
```shell
# サンプルコードのダウンロード
git clone https://github.com/JetBrains/intellij-sdk-code-samples.git
```

### サンプルに添付のパッチを当てる

```shell
cd intellij-sdk-code-samples/
patch -p1 < xxxxx.txt
```

<details><summary>パッチ（適当な名前で保存してください）</summary><div>

```diff
diff --git a/editor_basics/build.gradle b/editor_basics/build.gradle
index 98436d90..ff5da234 100644
--- a/editor_basics/build.gradle
+++ b/editor_basics/build.gradle
@@ -3,8 +3,11 @@
 plugins {
   id 'java'
   id 'org.jetbrains.intellij' version '1.3.1'
+  id 'org.jetbrains.kotlin.jvm' version '1.3.41'
+}
+tasks.withType(JavaCompile) {
+  options.encoding = 'UTF-8'
 }
-
 group 'org.intellij.sdk'
 version '2.0.0'
 
diff --git a/editor_basics/src/main/java/org/intellij/sdk/editor/DocStringTemplate.kt b/editor_basics/src/main/java/org/intellij/sdk/editor/DocStringTemplate.kt
new file mode 100644
index 00000000..b0768526
--- /dev/null
+++ b/editor_basics/src/main/java/org/intellij/sdk/editor/DocStringTemplate.kt
@@ -0,0 +1,24 @@
+package org.intellij.sdk.editor
+
+object DocStringTemplate {
+    @JvmStatic
+    val s = """
+        \"\"\"
+        Parameters       
+        ----------
+        {%parameters}
+        
+        Returns
+        -------
+        :
+        \"\"\"
+        """.trimIndent().replace("\\","")
+
+}
+
+object ParameterTemplate {
+    @JvmStatic
+    val s = """
+        {%parameter} : {%type}{%default}
+        """.trimIndent()
+}
\ No newline at end of file
diff --git a/editor_basics/src/main/java/org/intellij/sdk/editor/EditorAreaIllustration.java b/editor_basics/src/main/java/org/intellij/sdk/editor/EditorAreaIllustration.java
index 5cfa1c5f..3deb8e2d 100644
--- a/editor_basics/src/main/java/org/intellij/sdk/editor/EditorAreaIllustration.java
+++ b/editor_basics/src/main/java/org/intellij/sdk/editor/EditorAreaIllustration.java
@@ -16,7 +16,6 @@ import org.jetbrains.annotations.NotNull;
  * @see AnAction
  */
 public class EditorAreaIllustration extends AnAction {
-
   /**
    * Displays a message with information about the current caret.
    *
@@ -24,6 +23,8 @@ public class EditorAreaIllustration extends AnAction {
    */
   @Override
   public void actionPerformed(@NotNull final AnActionEvent e) {
+    if (true) return;
+     e.getInputEvent().isAltDown();
     // Get access to the editor and caret model. update() validated editor's existence.
     final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
     final CaretModel caretModel = editor.getCaretModel();
@@ -50,6 +51,7 @@ public class EditorAreaIllustration extends AnAction {
    */
   @Override
   public void update(@NotNull final AnActionEvent e) {
+    if (true) return;
     // Get required data keys
     final Project project = e.getProject();
     final Editor editor = e.getData(CommonDataKeys.EDITOR);
diff --git a/editor_basics/src/main/java/org/intellij/sdk/editor/EditorIllustrationAction.java b/editor_basics/src/main/java/org/intellij/sdk/editor/EditorIllustrationAction.java
index 1664cd02..d5609b0b 100644
--- a/editor_basics/src/main/java/org/intellij/sdk/editor/EditorIllustrationAction.java
+++ b/editor_basics/src/main/java/org/intellij/sdk/editor/EditorIllustrationAction.java
@@ -19,50 +19,51 @@ import org.jetbrains.annotations.NotNull;
  */
 public class EditorIllustrationAction extends AnAction {
 
-  /**
-   * Replaces the run of text selected by the primary caret with a fixed string.
-   *
-   * @param e Event related to this action
-   */
-  @Override
-  public void actionPerformed(@NotNull final AnActionEvent e) {
-    // Get all the required data from data keys
-    // Editor and Project were verified in update(), so they are not null.
-    final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
-    final Project project = e.getRequiredData(CommonDataKeys.PROJECT);
-    final Document document = editor.getDocument();
-    // Work off of the primary caret to get the selection info
-    Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
-    int start = primaryCaret.getSelectionStart();
-    int end = primaryCaret.getSelectionEnd();
-    // Replace the selection with a fixed string.
-    // Must do this document change in a write action context.
-    WriteCommandAction.runWriteCommandAction(project, () ->
-            document.replaceString(start, end, "editor_basics")
-    );
-    // De-select the text range that was just replaced
-    primaryCaret.removeSelection();
-  }
+    /**
+     * Replaces the run of text selected by the primary caret with a fixed string.
+     *
+     * @param e Event related to this action
+     */
+    @Override
+    public void actionPerformed(@NotNull final AnActionEvent e) {
+        if (true) return;
+        // Get all the required data from data keys
+        // Editor and Project were verified in update(), so they are not null.
+        final Editor editor = e.getRequiredData(CommonDataKeys.EDITOR);
+        final Project project = e.getRequiredData(CommonDataKeys.PROJECT);
+        final Document document = editor.getDocument();
+        // Work off of the primary caret to get the selection info
+        Caret primaryCaret = editor.getCaretModel().getPrimaryCaret();
+        int start = primaryCaret.getSelectionStart();
+        int end = primaryCaret.getSelectionEnd();
+        // Replace the selection with a fixed string.
+        // Must do this document change in a write action context.
+        WriteCommandAction.runWriteCommandAction(project, () ->
+                document.replaceString(start, end, "editor_basics")
+        );
+        // De-select the text range that was just replaced
+        primaryCaret.removeSelection();
+    }
 
-  /**
-   * Sets visibility and enables this action menu item if:
-   * <ul>
-   *   <li>a project is open</li>
-   *   <li>an editor is active</li>
-   *   <li>some characters are selected</li>
-   * </ul>
-   *
-   * @param e Event related to this action
-   */
-  @Override
-  public void update(@NotNull final AnActionEvent e) {
-    // Get required data keys
-    final Project project = e.getProject();
-    final Editor editor = e.getData(CommonDataKeys.EDITOR);
-    // Set visibility and enable only in case of existing project and editor and if a selection exists
-    e.getPresentation().setEnabledAndVisible(
-            project != null && editor != null && editor.getSelectionModel().hasSelection()
-    );
-  }
+    /**
+     * Sets visibility and enables this action menu item if:
+     * <ul>
+     *   <li>a project is open</li>
+     *   <li>an editor is active</li>
+     *   <li>some characters are selected</li>
+     * </ul>
+     *
+     * @param e Event related to this action
+     */
+    @Override
+    public void update(@NotNull final AnActionEvent e) {
+        // Get required data keys
+        final Project project = e.getProject();
+        final Editor editor = e.getData(CommonDataKeys.EDITOR);
+        // Set visibility and enable only in case of existing project and editor and if a selection exists
+        e.getPresentation().setEnabledAndVisible(
+                project != null && editor != null && editor.getSelectionModel().hasSelection()
+        );
+    }
 
 }
diff --git a/editor_basics/src/main/java/org/intellij/sdk/editor/MyTypedHandler.java b/editor_basics/src/main/java/org/intellij/sdk/editor/MyTypedHandler.java
index defcc830..2316f9e7 100644
--- a/editor_basics/src/main/java/org/intellij/sdk/editor/MyTypedHandler.java
+++ b/editor_basics/src/main/java/org/intellij/sdk/editor/MyTypedHandler.java
@@ -4,29 +4,279 @@ package org.intellij.sdk.editor;
 
 import com.intellij.codeInsight.editorActions.TypedHandlerDelegate;
 import com.intellij.openapi.command.WriteCommandAction;
-import com.intellij.openapi.editor.Document;
-import com.intellij.openapi.editor.Editor;
+import com.intellij.openapi.editor.*;
 import com.intellij.openapi.project.Project;
 import com.intellij.psi.PsiFile;
 import org.jetbrains.annotations.NotNull;
 
+import java.io.BufferedReader;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /**
  * This is a custom {@link TypedHandlerDelegate} that handles actions activated keystrokes in the editor.
  * The execute method inserts a fixed string at Offset 0 of the document.
  * Document changes are made in the context of a write action.
  */
 class MyTypedHandler extends TypedHandlerDelegate {
+    static long first = 0;
+    static long count = 0;
+
+    @NotNull
+    @Override
+    public Result charTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
+//        FileWriter writer;
+        int offset;
+        String contents;
+        String insert_str;
+        int curColumn;
+//        try {
+//            writer = new FileWriter("D:\\out.txt", StandardCharsets.UTF_8, true);
+//            writer.write("c:" + c + "\n");
+        if (c != ';') {
+//                writer.close();
+            return Result.CONTINUE;
+        }
+        long cur = System.currentTimeMillis();
+//            writer.write("cur:" + cur + " first:" + first);
+        if ((cur - first) > 1500) {
+            first = cur;
+            count = 1;
+//                writer.write("is first");
+//                writer.close();
+            return Result.STOP;
+        } else {
+            count++;
+//                writer.write("not first:" + count);
+            if (count != 3) {
+//                    writer.close();
+                return Result.STOP;
+            }
+        }
+//            writer.write("count is 3");
+        CaretModel caret = editor.getCaretModel();
+        VisualPosition position = caret.getVisualPosition();
+//            LogicalPosition position = caret.getLogicalPosition();
+//            offset = editor.logicalPositionToOffset(position);
+
+        offset = editor.visualPositionToOffset(position);
+        contents = file.getText();
+
+        String befLine = null;
+        String curLine = null;
+        try (BufferedReader reader = new BufferedReader(new StringReader(contents))) {
+            String line;
+            int cnt = 0;
+            while ((line = reader.readLine()) != null) {
+                if ((position.line - 1) == cnt) {
+                    befLine = line;
+                } else if (position.line == cnt) {
+                    curLine = line;
+                }
+                cnt++;
+            }
+        } catch (IOException ignored) {
+//                writer.write("exception\n");
+//                writer.close();
+            return Result.CONTINUE;
+        }
+        if ((befLine == null) || (curLine == null)) {
+//                writer.write("line check ng");
+//                writer.close();
+            return Result.CONTINUE;
+        }
+
+        // 上が空行は無視
+        if (befLine.matches("^\\s*$")) {
+//                writer.write("above is null");
+//                writer.close();
+            return Result.CONTINUE;
+        }
+
+        // 他に入力文字が有ったら無視
+        if (!curLine.matches("^\\s*;;\\s*$")) {
+//                writer.write("not ;;");
+//                writer.close();
+            return Result.CONTINUE;
+        }
+
+        curColumn = position.column;
+//            writer.write("ccc" + curColumn + "\n");
+//            writer.close();
+        insert_str = getInsertStr(befLine, curColumn - (curColumn % 4));
+//        } catch (IOException ignored) {
+//            return Result.CONTINUE;
+//        }
+
+
+        // Get the document and project
+        final Document document = editor.getDocument();
+        // Construct the runnable to substitute the string at offset 0 in the document
+        document.replaceString((offset - 3), offset, "");
+        String finalInsert_str = insert_str;
+//        try {
+//            writer = new FileWriter("D:\\out.txt", StandardCharsets.UTF_8, true);
+//            writer.write("insert:" + finalInsert_str + "\n");
+//            writer.close();
+//        } catch (IOException ignored) {
+//        }
+        Runnable runnable = () -> document.insertString(offset - 3, finalInsert_str + "\n");
+        // Make the document change in the context of a write action.
+        WriteCommandAction.runWriteCommandAction(project, runnable);
+        return Result.STOP;
+    }
+
+    /**
+     * 複数行の文字列の1行毎にインデントを追加する
+     *
+     * @param str    複数行の文字列
+     * @param indent 挿入するスペース数
+     * @return インデントを追加した文字列
+     */
+    String addIndent(String str, int indent) {
+        String indent_str = getIndentStr(indent);
+        StringBuilder retStr = new StringBuilder();
+        try (BufferedReader reader = new BufferedReader(new StringReader(str))) {
+            String line;
+            int cnt = 0;
+            while ((line = reader.readLine()) != null) {
+                if (cnt++ > 0) {
+                    retStr.append(indent_str);
+                }
+                retStr.append(line).append("\n");
+            }
+        } catch (IOException ignored) {
+        }
+
+        return retStr.toString();
+    }
+
+
+    /**
+     * 指定した数値に応じた連続スペースを返却する
+     *
+     * @param indent インデント位置
+     * @return 連続スペース
+     */
+    String getIndentStr(int indent) {
+        if (indent <= 0) return "";
+        return " ".repeat(indent);
+    }
+
+    /**
+     * すぐ上の行に応じたdocstringを返す
+     *
+     * @param befLine カーソルの上の行
+     * @return docstring
+     */
+    String getInsertStr(String befLine, int indent) {
+        String pattern = "^\\s*def\\s.*\\((.*)\\).*\\s*$";
+        Pattern p = Pattern.compile(pattern);
+        Matcher m = p.matcher(befLine);
+        String template = DocStringTemplate.getS();
+        String str = null;
+//        FileWriter writer;
+
+//        try {
+//            writer = new FileWriter("D:\\out.txt", StandardCharsets.UTF_8, true);
+            if (m.find()) {
+                // 関数の場合
+                String paramTemplate = ParameterTemplate.getS();
+
+                String args_str = m.group(1).replaceAll("\\s", "");
+//                writer.write("args_str:" + args_str + "\n");
+                String[] args_ary = args_str.split(",");
+                List<Arg> args = new ArrayList<>();
+//                writer.write("args_ary:" + args_ary.length);
+                for (String arg_str : args_ary) {
+                    if (arg_str.length() == 0) continue;
+                    String[] temp = arg_str.split("=");
+//                    writer.write("temp=:" + temp[0] + "\n");
+                    Arg arg = new Arg();
+                    if (temp.length > 1) {
+                        arg.def = temp[1];
+                    }
+                    temp = temp[0].split(":");
+//                    writer.write("temp::" + temp[0] + "\n");
+                    if (temp.length > 1) {
+                        arg.type = temp[1];
+                    }
+                    arg.name = temp[0];
+                    if (!"self".equals(arg.name))
+                        args.add(arg);
+                }
 
-  @NotNull
-  @Override
-  public Result charTyped(char c, @NotNull Project project, @NotNull Editor editor, @NotNull PsiFile file) {
-    // Get the document and project
-    final Document document = editor.getDocument();
-    // Construct the runnable to substitute the string at offset 0 in the document
-    Runnable runnable = () -> document.insertString(0, "editor_basics\n");
-    // Make the document change in the context of a write action.
-    WriteCommandAction.runWriteCommandAction(project, runnable);
-    return Result.STOP;
-  }
+                if (args.size() > 0) {
+//                    writer.write("args.size > 0\n");
+                    // 引数有りパターン
+                    StringBuilder strParams = new StringBuilder();
+                    for (Arg arg : args) {
+//                        writer.write("name:" + arg.name + " type:" + arg.type + " def:" + arg.type + "\n");
+                        String strParam = paramTemplate;
+                        strParam = strParam.replaceAll("\\{%parameter}", arg.name);
+//                        writer.write("pend:" + strParam +"\n");
+                        strParam = strParam.replaceAll("\\{%type}", arg.type);
+                        String def = arg.def.length() > 0 ? ", default " + arg.def : "";
+                        strParam = strParam.replaceAll("\\{%default}", def);
+//                        writer.write("comp:" + strParam +"\n");
+                        strParams.append(strParam).append("\n");
+                    }
+                    str = template.replaceAll("\\{%parameters}", strParams.toString());
+                } else {
+//                    writer.write("args.size == 0");
+                    // 引数なしパターン
+                    // java は複数行の部分置換が出来ない
+                    String p_str = "Parameters";
+                    String r_str = "Returns";
+                    int p_len = p_str.length();
+                    int r_len = r_str.length();
+                    int len = template.length();
+                    int d_start = 0;
+                    int d_end = 0;
+                    for (int i = 0; i < len - r_len; i++) {
+                        if (((len - i) > p_len) && (p_str.equals(template.substring(i, i + p_len)))) {
+                            d_start = i;
+                        } else if (((len - i) > r_len) && (r_str.equals(template.substring(i, i + r_len)))) {
+                            d_end = i;
+                            break;
+                        }
+                    }
+                    if (d_end > d_start) {
+                        str = template.substring(0, d_start) + template.substring(d_end);
+                    }
+                }
+            } else {
+                // 関数以外の場合
+                str = "\"\"\"\n" + "\"\"\"\n";
+            }
+//            writer.write("indent:" + getIndentStr(indent) + "\n");
+//            writer.close();
+//        } catch (IOException ignored) {
+//        }
+        return addIndent(str, indent);
+    }
 
+    /**
+     * python の 引数1つ分
+     */
+    static class Arg {
+        /**
+         * 引数名
+         */
+        String name = "";
+        /**
+         * 型ヒント
+         */
+        String type = "";
+        /**
+         * default値
+         */
+        String def = "";
+    }
 }

```

</div></details>

### ビルドする
```shell
# JAVAのパスは環境に合わせる。
cd editor_basics
JAVA_HOME=/d/Java/jdk-11.0.12/ ./gradlew buildPlugin
```
editor_basics/build/distributions/editor-2.0.0.zip が作成される

### PyCharmのpluginに設定する
PyCharmからFile -> Settings -> Plugins と選ぶ。
Installedの右の歯車からInstall Plugin from Diskを選択する。
作成されたzipファイルを選択する。
OKを押してSettings画面を閉じる。

### 使い方
;;;（関数定義の直下の行でセミコロンを1.5s以内に3連打するとコメントテンプレートが挿入される）

<details><summary>こんな感じ</summary><div>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2383810/54643a03-8ad8-fc34-be00-2dd34c7090e4.png)
</div></details>

## 1から作ろうとして断念した経緯

[この辺](https://qiita.com/Vexus2/items/e04a21f00e467b7ac8ad)とか[これ](https://qiita.com/itohiro73/items/d46dd3b05a3ca30226f4)とか
書いてあるのを参考にしようとしたけど

* src 右クリでactionが出てこない
* gradleとかxmlの編集とかよくわからない

とかの理由で誰かが作ってるの編集した方が速いのでは？と思いました。

## はまったポイント
gradle実行時のjavaのversionが11(以上かな？)で無いと動かない
"（ダブルクォーテーション）は飛んでこない