美文网首页
【不务正业】如何开发一个摸鱼插件

【不务正业】如何开发一个摸鱼插件

作者: 星星y | 来源:发表于2020-08-28 13:46 被阅读0次

    前言

    摸鱼应该是程序员必备的技能了吧,每个人都有自己的一套摸鱼方式。
    甚至有些摸鱼软件能让你更加愉悦的上班。前段时间无意接触的Thief,感觉挺有意思的,但是小说阅读体验不是很好。
    于是利用摸鱼的时间自己写了个摸鱼的插件。官方文档和例子也不是很完全,在查看flutter,junit,leetcode-editor等相关idea插件源码后才慢慢有点经验。
    以下是idea插件开发的相关笔记,感兴趣的通过可以了解下(也可以用下这款插件)。

    基于DevKit开发摸鱼插件简易版

    开发环境

    • idea2018.2.4
    • devkit

    代码地址

    ReaderPlugin-devkit分支

    ToolWindowFactory

    使用idea2018,新建一个devkit插件项目


    devkit

    然后在resources/META-INF/plugin.xml文件下添加toolWindow结点用于展示文本阅读器界面

    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
        <toolWindow id="iReader" anchor="bottom" canCloseContents="true"
                    factoryClass="com.iamyours.reader.MainUi"/>
    </extensions>
    

    其中factoryClassToolWindowFactory接口的实现类,新建MainUi实现它。

    public class MainUI implements ToolWindowFactory {
        @Override
        public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
            JPanel mainPanel = new JPanel();
            // ... 添加阅读器布局
            initUI(mainPanel);
            //将mainPanel加入到ToolWindow中
            ContentFactory contentFactory = ContentFactory.SERVICE.getInstance();
            Content content = contentFactory.createContent(mainPanel, "Debug", false);
            toolWindow.getContentManager().addContent(content);
        }
    }
    

    我们可以在mainPanel中加入基于Swing的ui控件,并添加相应的事件,最终实现了一个小说阅读器。

    基于gradle实现的可运行版阅读插件

    基于devkit的版本实现起来简单,我们可以用它来摸鱼了。有一个问题就是不够隐蔽,不够"装模作样"。于是就有了第二种版本:通过写代码的方式运行插件,
    这样就能无中生有暗度陈仓凭空想象浑水摸鱼
    首先创建一个测试类Test,里面添加一个摸鱼函数,加入要阅读的txt文件。

    public class Test {
        public void fishReadTxt(){
            String path = "/Users/xxx/Downloads/凡人修仙传.txt";
        }
    }
    

    此时插件自动识别定位该方法,点击左边icon,显示Run fishRead2运行摸鱼程序。

    开发环境

    • idea2020.2 CE
    • gradle

    代码地址

    ReaderPlugin-master分支

    gradle配置

    使用idea2020新建一个gradle项目,勾选Intellij platform plugin,在build.gradle文件中配置如下

    intellij {
        //version = '2020.2' 
        plugins = ['java']  //处理java源码
        updateSinceUntilBuild false //兼容idea旧版本
        localPath "/Users/xxx/Desktop/soft/ideaIC-2018.2.4"  //下载较慢,使用本地idea版本
    }
    

    lineMarker显示摸鱼函数

    为了能找到以fish开头的摸鱼方法,我们在plugin.xml中添加runLineMarkerContributor,并且添加实现类FishLineMarker

    <extensions defaultExtensionNs="com.intellij">
        <!-- Add your extensions here -->
        <runLineMarkerContributor language="JAVA" implementationClass="com.iamyours.reader.run.FishLineMarker"/>
        <runConfigurationProducer implementation="com.iamyours.reader.run.FishProducer"/>
        <configurationType implementation="com.iamyours.reader.run.FishConfigType"/>
    </extensions>
    
    public class FishLineMarker extends RunLineMarkerContributor {
        @Override
        public @Nullable Info getInfo(@NotNull PsiElement element) {
            if (element instanceof PsiJavaToken && element.getParent() instanceof PsiMethod) {//指定方法那一行
                PsiMethod psiMethod = (PsiMethod) element.getParent();
                String name = psiMethod.getName();
                if (name.startsWith("fish")) {//标记fish开头的方法
                    final Icon icon = ReaderIcons.LOGO;
                    PsiClass psiClass = (PsiClass) psiMethod.getParent();
                    String classAndMethod = psiClass + "." + name + "()";
                    final Function<PsiElement, String> tooltipProvider =
                            psiElement -> "Run '" + classAndMethod + "'";
                    return new RunLineMarkerContributor.Info(icon, tooltipProvider, ExecutorAction.getActions());
                }
            }
            return null;
        }
    }
    

    getInfo方法会得到java类中的所有元素,包括方法,成员变量,本地变量,表达式。因此你可以标记任意一行代码。这里我们只关注方法,并且是以fish为开头的方法。

    linemarker.png

    configurationType

    通过idea中的Edit Configuration,可以添加,修改各种运行程序。如tomcat,junit,android等。你会在Configration下发现各种模版。
    我们通过configurationType添加自己的configuration,用于保存摸鱼插件的相关信息(如txt文件路径),对应的实现类如下:

    public class FishConfigType implements ConfigurationType {
        final ConfigurationFactory factory = new Factory(this);
    
        private static final FishConfigType instance = new FishConfigType();
    
        public static FishConfigType getInstance() {
            return instance;
        }
    
       //...其他重写方法,如显示名称,icon,描述等
    
        @Override
        public ConfigurationFactory[] getConfigurationFactories() {
            return new ConfigurationFactory[]{factory};
        }
    
        static class Factory extends ConfigurationFactory {
            protected Factory(@NotNull FishConfigType type) {
                super(type);
            }
    
            @Override
            public @NotNull RunConfiguration createTemplateConfiguration(@NotNull Project project) {
                return new FishRunConfiguration(project, this, "");
            }
        }
    }
    

    定义RunConfiguration设置界面

    public class FishRunConfiguration extends LocatableConfigurationBase {
        protected FishRunConfiguration(@NotNull Project project, @NotNull ConfigurationFactory factory, @Nullable String name) {
            super(project, factory, name);
        }
        @Override
        public @NotNull SettingsEditor<? extends RunConfiguration> getConfigurationEditor() {
            return new FishRunConfigUI();
        }
    
        @Override
        public @Nullable RunProfileState getState(@NotNull Executor executor, @NotNull ExecutionEnvironment executionEnvironment) throws ExecutionException {
            return new FishRunState(executionEnvironment, this);//具体插件运行逻辑,界面
        }
    }
    

    每个RunConfiguration的设置界面是可以配置的。可通过getConfigurationEditor添加SettingsEditor

    public class FishRunConfigUI extends SettingsEditor<FishRunConfiguration> {
        private JPanel form;
    
        @Override
        protected void resetEditorFrom(@NotNull FishRunConfiguration config) {
           
        }
    
        @Override
        protected void applyEditorTo(@NotNull FishRunConfiguration config) throws ConfigurationException {
          
        }
    
        @Override
        protected @NotNull JComponent createEditor() {
            return form;
        }
    }
    

    右键选择new->Swing UI Designer->GUI Form创建一个FishRunConfigUI表单界面,并将它继承自SettingsEditor
    有三个重要的方法:

    • createEditor: 创建RunConfiguration设置界面
    • resetEditorFrom:将数据从config中恢复到界面
    • applyEditorTo:将ui中的数据保存到config中。


      run-configuration.png

    数据保存

    为了能够在下次打开ide时还能运行RunConfigration中的程序,需要将config中的数据保存到磁盘。我们通过FishRunConfiguration中的writeExternalreadExternal来写入或读取数据。

    public class FishRunConfiguration extends LocatableConfigurationBase {
        @Override
        public void writeExternal(@NotNull Element element) {
            super.writeExternal(element);
            ElementIO.addOption(element, "bookPath", bookPath);
            ElementIO.addOption(element, "classFile", classFile);
    
        }
    
        @Override
        public void readExternal(@NotNull Element element) throws InvalidDataException {
            super.readExternal(element);
            bookPath = ElementIO.readOptions(element).get("bookPath");
            classFile = ElementIO.readOptions(element).get("classFile");
        }
    }
    
    
    /**
     * Utilities for reading and writing IntelliJ run configurations to and from the disk.
     */
    public class ElementIO {
    
      public static void addOption(@NotNull Element element, @NotNull String name, @Nullable String value) {
        if (value == null) return;
    
        final Element child = new Element("option");
        child.setAttribute("name", name);
        child.setAttribute("value", value);
        element.addContent(child);
      }
    
      public static Map<String, String> readOptions(Element element) {
        final Map<String, String> result = new HashMap<>();
        for (Element child : element.getChildren()) {
          if ("option".equals(child.getName())) {
            final String name = child.getAttributeValue("name");
            final String value = child.getAttributeValue("value");
            if (name != null && value != null) {
              result.put(name, value);
            }
          }
        }
        return result;
      }
    }
    

    使用runConfigurationProducer创建RunConfiguration

    通过之前的配置,我们可以通过Edit Congigurations方式添加一个RunConfiguration来运行插件。
    然后在FishRunConfiguration中的getState中来实现具体插件的运行逻辑,这个逻辑暂且放到后面。这里需要注意的是我们已经通过runLineMarkerContributor标记了摸鱼方法,但是并没有实现对应的操作。
    所以需要通过runConfigurationProducer来创建(或修改)RunCongfiguration

    public class FishProducer extends RunConfigurationProducer<FishRunConfiguration> {//指定的Configuration为FishRunConfiguration
        public FishProducer() {
            super(new FishConfigType());//具体
        }
    
        @Override
        protected boolean setupConfigurationFromContext(//根据上下文判断是否要添加到RunConfiguration
                @NotNull FishRunConfiguration config,
                @NotNull ConfigurationContext context,
                @NotNull Ref<PsiElement> ref) {
            final PsiElement element = context.getPsiLocation();
            if (element instanceof PsiJavaToken && element.getParent() instanceof PsiMethod) {//
                PsiMethod psiMethod = (PsiMethod) element.getParent();
                String name = psiMethod.getName();
                if (name.startsWith("fish")) {//当前为fish方法时,添加到RunConfiguration
                    //接着获取方法中的一些参数
                    return true;
                }
            }
            return false;
        }
        
    
        @Override
        public boolean isConfigurationFromContext(
                @NotNull FishRunConfiguration config,
                @NotNull ConfigurationContext context) {//判断是否要修改RunConfiguration
            String name = config.getName();
            final PsiElement element = context.getPsiLocation();
            if (element instanceof PsiJavaToken && element.getParent() instanceof PsiMethod) {
                PsiMethod psiMethod = (PsiMethod) element.getParent();
                String methodName = psiMethod.getName();
                if (methodName.startsWith("fish")
                        && methodName.equals(config.getName())) {
                    return isInConfigs(context, psiMethod);
                }
            }
            return false;
        }
    
        private boolean isInConfigs(ConfigurationContext context, PsiMethod psiMethod) {
            List<RunConfiguration> list = context.getRunManager().getAllConfigurationsList();
            String name = psiMethod.getName();
            for (RunConfiguration config : list) {
                if (config instanceof FishRunConfiguration
                        && config.getName().equals(name)) {//如果在RunConfiguration列表中有,则修改相关信息
                    FishRunConfiguration rc = (FishRunConfiguration) config;
                    return true;
                }
                ;
            }
            return false;
        }
    }
    

    RunProfileState具体插件运行逻辑

    我们在RunConfiguraion中已经保存了运行插件需要的相关参数,接下来我们实现具体的插件逻辑。还记得之前FishRunConfiguration类中的getState方法吗?
    我们需要获取一个RunProfileState实例,里面是插件运行的逻辑。

    public class FishRunState implements RunProfileState {
        private FishRunConfiguration config;
        private ExecutionEnvironment environment;
    
        protected FishRunState(ExecutionEnvironment environment, FishRunConfiguration config) {
            this.config = config;
            this.environment = environment;
        }
    
        private FishRunConsole console;//插件运行界面
    
        private void doRun(ProcessHandler handler, FishRunConfiguration config) {
            ProgressManager.getInstance().run(new Task.Backgroundable(environment.getProject(), "build", true) {
                @Override
                public void run(@NotNull ProgressIndicator progressIndicator) {
                    SwingUtilities.invokeLater(() -> {
                        console.startBuild(config);
                    });
                    BookEngine engine = new BookEngine(config.bookVO);
                    SwingUtilities.invokeLater(() -> {
                        console.loadWithEngine(engine);
                    });
                    handler.destroyProcess();//结束运行
                }
            });
        }
    
        @Nullable
        @Override
        public ExecutionResult execute(Executor executor, @NotNull ProgramRunner programRunner) throws ExecutionException {
            ProcessHandler handler = new NopProcessHandler();
            console = new FishRunConsole();
            DefaultExecutionResult result = new DefaultExecutionResult(console, handler) {
            };
            ProcessTerminatedListener.attach(handler);
            handler.addProcessListener(new ProcessListener() {
                @Override
                public void startNotified(@NotNull ProcessEvent processEvent) {
                    doRun(handler, config);
                }
    
                @Override
                public void processTerminated(@NotNull ProcessEvent processEvent) {}
    
                @Override
                public void processWillTerminate(@NotNull ProcessEvent processEvent, boolean b) {}
    
                @Override
                public void onTextAvailable(@NotNull ProcessEvent processEvent, @NotNull Key key) {}
            });
            return result;
        }
    }
    

    ExecutionConsole展示插件运行界面

    public class FishRunConsole implements ExecutionConsole, KeyListener {
        private JTree tree;
        private JTextArea textArea;
        private JComponent component;
        private DefaultMutableTreeNode root;
    
        @Override
        public @NotNull JComponent getComponent() {
            if (component == null) {
                component = createComponent();
            }
            return component;
        }
    
        @Override
        public JComponent getPreferredFocusableComponent() {
            return getComponent();
        }
    
        public void startBuild(FishRunConfiguration config) {
            root.add(new LoadingNode("...."));
            textArea.requestFocus();
        }
    
        private JComponent createComponent() {
            SimpleToolWindowPanel panel = new SimpleToolWindowPanel(true);
            SimpleToolWindowPanel top = new SimpleToolWindowPanel(true);
            tree = new SimpleTree() {
                @Override
                protected void paintComponent(Graphics g) {
                    super.paintComponent(g);
                    DefaultMutableTreeNode root = (DefaultMutableTreeNode) treeModel.getRoot();
                }
            };
            ChapterVO obj = new ChapterVO(0, "build");
            root = new DefaultMutableTreeNode(obj);
            DefaultTreeModel model = new DefaultTreeModel(root);
            tree.setModel(model);
            tree.addTreeSelectionListener(new TreeSelectionListener() {
                @Override
                public void valueChanged(TreeSelectionEvent e) {
                    TreePath path = e.getPath();
                    DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent();
                    if (engine != null) {
                        engine.selectChapter((ChapterVO) node.getUserObject());
                        textArea.setText(engine.getCurrentContent());
                    }
                }
            });
            removeKeyListener(tree);//移除搜索相关事件
            tree.addKeyListener(this);
            tree.setCellRenderer(new CustomTreeRenderer());
            SimpleToolWindowPanel right = new EastToolWindowPanel(false);
            final ActionManager actionManager = ActionManager.getInstance();
            ActionToolbar actionToolbar = actionManager.createActionToolbar("Reader Toolbar",
                    (DefaultActionGroup) actionManager.getAction("reader.TextArea"),
                    true);
            actionToolbar.setTargetComponent(textArea);
            right.setToolbar(actionToolbar.getComponent());
            textArea = new JTextArea("press 'N' to next");
            textArea.setMargin(new Insets(5, 5, 5, 5));
            textArea.addKeyListener(this);
            textArea.setWrapStyleWord(true);
            textArea.setLineWrap(true);
            textArea.setEditable(false);
            JBScrollPane textScrollPane = new JBScrollPane(textArea);
            right.setContent(textScrollPane);
            JBSplitter splitPane = new OnePixelSplitter(false, "test", 0.3f);
            splitPane.setFirstComponent(top);
            splitPane.setSecondComponent(right);
            Color color = new Color(0, 0, 0, 0);
            splitPane.getDivider().setBackground(color);
            JBScrollPane scrollPane = new JBScrollPane(tree);
            scrollPane.setHorizontalScrollBarPolicy(HORIZONTAL_SCROLLBAR_NEVER);
            top.setContent(scrollPane);
            panel.setContent(splitPane);
            return panel;
        }
    
        private void removeKeyListener(JComponent component) {
            KeyListener[] listeners = component.getKeyListeners();
            for (KeyListener listener : listeners) {
                component.removeKeyListener(listener);
            }
        }
        
        private int count;
    
        @Override
        public void keyReleased(KeyEvent e) {
            String str = e.getKeyChar() + "";
            if ("N".equals(str.toUpperCase())) {
                engine.readNext();
                textArea.setText(engine.getCurrentContent());
            }
        }
    
        private BookEngine engine;
        private int currentChapterIndex = 0;
        private boolean expanded = true;
    
        private void selectCurrent() {
            if (!expanded) return;
            tree.setSelectionRow(currentChapterIndex);
            tree.scrollRowToVisible(currentChapterIndex);
        }
    
        public void loadWithEngine(BookEngine engine) {
            this.engine = engine;
            engine.setChapterListener(new BookEngine.ChapterListener() {
                @Override
                public void chapterChanged(int index) {
                    currentChapterIndex = index + 1;
                    selectCurrent();
                }
            });
            tree.addTreeExpansionListener(new TreeExpansionListener() {
                @Override
                public void treeExpanded(TreeExpansionEvent event) {
                    expanded = true;
                    selectCurrent();
                }
    
                @Override
                public void treeCollapsed(TreeExpansionEvent event) {
                    expanded = false;
                }
            });
            EventBus.register(() -> {
                engine.readNext();
                textArea.setText(engine.getCurrentContent());
            });
            root.removeAllChildren();
            for (ChapterVO c : engine.getChapterList()) {
                root.add(new DefaultMutableTreeNode(c));
            }
            tree.expandRow(0);
            tree.updateUI();
        }
    }
    

    最终运行界面如下:


    参考链接

    IntelliJ Platform SDK DevGuide
    intellij-sdk-code-samples
    junit插件
    flutter-intellij
    leetcode-editor

    相关文章

      网友评论

          本文标题:【不务正业】如何开发一个摸鱼插件

          本文链接:https://www.haomeiwen.com/subject/sjqlsktx.html