前言
摸鱼应该是程序员必备的技能了吧,每个人都有自己的一套摸鱼方式。
甚至有些摸鱼软件能让你更加愉悦的上班。前段时间无意接触的Thief,感觉挺有意思的,但是小说阅读体验不是很好。
于是利用摸鱼的时间自己写了个摸鱼的插件。官方文档和例子也不是很完全,在查看flutter
,junit
,leetcode-editor
等相关idea插件源码后才慢慢有点经验。
以下是idea插件开发的相关笔记,感兴趣的通过可以了解下(也可以用下这款插件)。
基于DevKit开发摸鱼插件简易版
开发环境
- idea2018.2.4
- devkit
代码地址
ReaderPlugin-devkit分支
ToolWindowFactory
使用idea2018,新建一个devkit插件项目
然后在resources/META-INF/plugin.xml
文件下添加toolWindow
结点用于展示文本阅读器界面
其中factoryClass
为ToolWindowFactory
接口的实现类,新建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
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 tooltipProvider =
psiElement -> "Run '" + classAndMethod + "'";
return new RunLineMarkerContributor.Info(icon, tooltipProvider, ExecutorAction.getActions());
}
}
return null;
}
}
getInfo
方法会得到java类中的所有元素,包括方法,成员变量,本地变量,表达式。因此你可以标记任意一行代码。这里我们只关注方法
,并且是以fish
为开头的方法。
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 {
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中。
数据保存
为了能够在下次打开ide时还能运行RunConfigration
中的程序,需要将config中的数据保存到磁盘。我们通过FishRunConfiguration
中的writeExternal
和readExternal
来写入或读取数据。
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 readOptions(Element element) {
final Map 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 {//指定的Configuration为FishRunConfiguration
public FishProducer() {
super(new FishConfigType());//具体
}
@Override
protected boolean setupConfigurationFromContext(//根据上下文判断是否要添加到RunConfiguration
@NotNull FishRunConfiguration config,
@NotNull ConfigurationContext context,
@NotNull Ref 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 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