DevOps DevOps
产品简介
产品安装
快速入门
使用指南
开发指南
FAQ
6.7更新说明
  • idea插件开发说明
  • 插件项目新建
  • 插件的扩展开发
  • 插件打包导出
  • 插件版本兼容

# IDEA插件开发说明

# 插件项目新建

新建一个插件项目

1、在 "FILE->NEW->PROJECT" "New Project"(新建项目)对话框中,选择 "IDE Plugin"(IntelliJ 平台插件)项目类型。
2、请按需选择Type,这里我们的Devops4git插件是基于Theme,用于当前主题扩展。
3、在 "JDK" 下拉列表中,选择你将用于开发插件的 JDK 版本。建议使用与 IntelliJ IDEA 相同的 JDK 版本。
4、补充其他基本信息,点击 "create" 按钮完成插件项目的创建。

intellij-new-project

# 插件的扩展开发

扩展DevOps4Git插件项目

在Devops源码中包含名为devops-ide-idea的插件项目,目的是提供Settings中对DevOps平台的连接配置,在Git提交模块提供选择DevOps任务项功能。

1、idea插件项目导入
通过open选择插件项目
2、project structure
sdk选择编译器本身
language推荐选对应编译器Jdk
3、启动插件
run/debug configurations
plugin
jre选编译器本身
4、运行效果
settings首选项中能看到devops,项目运行成功。

下文将以该项目为例,介绍如何基础的插件开发流程。

intellij-devops4git-project

plugin.xml文件说明

plugin.xml 文件的一些主要元素和用途:

<idea-plugin>:这是 plugin.xml 文件的根元素,用于指定插件的基本信息和属性。
<id>:指定插件的唯一标识符(ID),用于区分和标识插件。该 ID 需要在 IntelliJ IDEA 插件市场中是唯一的。
<name>:指定插件的名称,这将是插件在 IntelliJ IDEA 中显示的名称。
<version>:指定插件的版本号,用于跟踪和管理插件的不同版本。
<vendor>:指定插件的开发者或供应商的信息,包括名称、电子邮件地址等。
<description>:指定插件的简要描述,用于向用户提供关于插件功能的信息。
<change-notes>:指定插件版本更新的变更说明,用于向用户显示插件的新功能和改进。
<idea-version>:指定插件所支持的 IntelliJ IDEA 版本范围,以确保插件在不同版本的 IntelliJ IDEA 中能够正常工作。
<depends>:指定插件所依赖的其他插件或库,以确保在安装和使用插件之前,相关的依赖项已经安装和可用。
<extensions> 和 <extensionPoints>:指定插件扩展 IntelliJ IDEA 功能的方式和扩展点。插件可以使用 IntelliJ Platform API 提供的扩展点来添加自定义的功能和行为。
<actions>:指定插件中定义的操作和菜单项。插件可以添加自定义的操作到 IntelliJ IDEA 的菜单、工具栏或上下文菜单中。
<applicationComponents>、<projectComponents> 和 <moduleComponents>:指定插件中定义的应用程序、项目或模块级别的组件。这些组件可以在插件的生命周期中执行特定的操作或提供额外的功能。

工具栏扩展流程说明

以DevOps4Git插件为例,我们想要实现左侧工具栏能展示devops项目的工作项列表。
1、我们需要在plugin.xml文件内 <extensions> 标签中添加 <toolWindow> 标签用于扩展IntelliJ IDEA的工具窗口

toolWindow标签属性说明如下

id:指定工具窗口的唯一标识符(ID),用于区分和标识工具窗口。该 ID 需要在插件内部是唯一的。
anchor:指定工具窗口的锚点位置,即工具窗口在 IntelliJ IDEA 窗口中的停靠位置。可以选择的锚点位置包括左侧、右侧、顶部和底部。
factoryClass:指定工具窗口的工厂类,该类负责创建和初始化工具窗口的内容。工厂类需要实现 ToolWindowFactory 接口,并提供相应的实现方法。
icon:指定工具窗口的图标,该图标将显示在工具窗口的标题栏上。可以使用相对路径或绝对路径来指定图标文件的位置。
secondary:指定工具窗口是否为辅助工具窗口。辅助工具窗口通常显示在主要工具窗口的旁边,并且具有较小的尺寸和简化的功能。
stripToolWindow:指定工具窗口是否显示在 "Window"(窗口)菜单的工具窗口列表中。如果设置为 true,则工具窗口将不会显示在菜单中。
contentUiType:指定工具窗口内容的 UI 类型。可以选择的 UI 类型包括 "com.intellij.ui.tabs.JBTabsUI"(选项卡式 UI)和 "com.intellij.ui.tabs.impl.JBTabsImpl"(标签页式 UI)。
alwaysOnTop:指定工具窗口是否始终显示在其他工具窗口的上方。如果设置为 true,则工具窗口将始终保持在最前面。

示例代码如下

<extensions defaultExtensionNs="com.intellij">
    <toolWindow id="Workitems" anchor="left" factoryClass="com.primeton.devops.ide.idea.toolWindow.WorkitemToolWindowFactory" secondary="false" icon="/icons/workitems.png"/>
</extensions>

2、新建一个实现了ToolWindowFactory的工厂类放入上文factoryClass中,其中WorkitemToolWindows是需要展示的窗口内容

intellij-new-toolwindow-factory

示例代码如下

public class WorkitemToolWindowFactory implements ToolWindowFactory {

    @Override
    public void createToolWindowContent(@NotNull Project project, @NotNull ToolWindow toolWindow) {
        WorkitemToolWindow myToolWindow = new WorkitemToolWindow(project);
        ContentFactory contentFactory = ContentFactory.getInstance();
        Content content = contentFactory.createContent(myToolWindow.getContent(), "工作项列表", false);
        toolWindow.getContentManager().addContent(content);

    }
}

3、进行WorkitemToolWindow界面设计

对目标文件夹右键 "New->Swing Ui Designer->GUI Form"建立一个 Swing 窗体。

intellij-new-view-form

对于生成的WorkitemToolWindow.form文件可以使用Swing UI Designer 的可视化编辑器来设计和布局窗体,在WorkitemToolWindow.java文件中实现交互。

intellij-form

4、WorkitemToolWindow界面功能实现

在WorkitemToolWindow.java文件我们可以完成对绘制界面的数据处理已经按钮响应。

以WorkitemToolWindow.java文件为例介绍实现流程,代码如下

/**
 * 工作项列表界面
 */
public class WorkitemToolWindow extends JDialog implements DevopsConstant {
    private JPanel contentPane;
    private JTextField searchField;
    private JTable workitemTable;
    private JComboBox projectCombo;
    private JButton refreshButton;
    private Map<String, String> dictPriority = new HashMap<String, String>();
    private Map<String, String> dictBugLevel = new HashMap<String, String>();
    private Settings settings = Settings.getInstance();

    private Map<String, String> settingMap;
    private DefaultTableModel baseModel;
    private Project project;

    public WorkitemToolWindow(Project project) {
        this.project = project;
        init();
        addListeners();

    }

    // 页面初始化
    protected void init() {
        searchField.setToolTipText("输入Key或者标题搜索");
        settingMap = settings.getSettingMap();
        initProject(settingMap);
        buildNewInput((ProjectDevops) projectCombo.getSelectedItem());
    }

    // 初始化项目下选择拉框
    private void initProject(Map<String, String> settingMap){
        DefaultTableModel model = (DefaultTableModel) workitemTable.getModel();
        model.setRowCount(0);
        String projectIds = settingMap.getOrDefault(devopsProjectIds, "");
        String projectUrl = MessageFormat.format(ApiUtil.API_GET_PROJECTS, projectIds);
        projectCombo.removeAllItems();
        // 获取项目列表
        Object result = HttpUtil.sendGet(projectUrl);
        // 添加项目到下拉框中
        if (result != null) {
            for (Object projectObj : (List<Object>) result) {
                if (projectObj instanceof Map) {
                    ProjectDevops project = new ProjectDevops((Map<String, Object>) projectObj);
                    projectCombo.addItem(project);
                }
            }
        }
    }

    // 加载列表数据
    private void buildNewInput(ProjectDevops project) {
        if (project==null) {
            return;
        }
        // 获取首选项中的devops配置
        Map<String, String> settingMap = settings.getSettingMap();
        String workitemStatuses = settingMap.getOrDefault(devopsStatus, "");
        // 请求接口获取工作项列表数据
        Object result = HttpUtil.sendPost(ApiUtil.API_GETWORKITEMS, ApiUtil.buildWorkItemsBody(project.getProjectId(), workitemStatuses));
        Object[][] data = null;
        // 设置图片基础路径
        String path = "";
        // 获取业务字典
        DictUtil dictUtil = DictUtil.getInstance();
        dictUtil.loadDict();
        dictPriority = dictUtil.getDictPriority();
        dictBugLevel = dictUtil.getDictBugLevel();
        // 处理返回的工作项列表数据
        if (result != null && result instanceof List) {
            List<Map<String, Object>> workitems = (List<Map<String,Object>>) result;
            data = new Object[workitems.size()][8];
            for (int i = 0; i < workitems.size(); i++) {
                Object[] objects = new Object[8];
                Map<String, Object> workitemType = (Map<String, Object>) workitems.get(i).get("workitemType");
                if (workitemType == null) {
                    objects[0] = path + "workItemCustom.png";
                } else {
                    String workitemTypeId = (String) workitemType.get("workitemTypeId");
                    if ( "2".equals(workitemTypeId) ) {
                        objects[0] = path + "workItemStory.png";
                    } else if ("3".equals(workitemTypeId)) {
                        objects[0] = path + "workItemTask.png";
                    } else if ("4".equals(workitemTypeId)) {
                        objects[0] = path + "workItemBug.png";
                    } else if ("5".equals(workitemTypeId)) {
                        objects[0] = path + "workItemRisk.png";
                    } else {
                        objects[0] = path + "workItemCustom.png";
                    }
                }
                objects[1] = workitems.get(i).get("workitemKey");
                objects[2] = workitems.get(i).get("title");
                objects[3] = dictPriority.get(workitems.get(i).get("priority"));
                objects[4] = dictBugLevel.get(workitems.get(i).get("bugLevel"));
                objects[5] = workitems.get(i).get("projectName");
                objects[6] = workitems.get(i).get("dueTime");
                objects[7] = workitems.get(i).get("workitemId");
                data[i] = objects;
            }
        } else {
            data = new Object[][]{};
        }
        // 初始化列表model
        baseModel = new DefaultTableModel(data, new Object[] { "", "Key", "标题", "优先级", "严重等级", "所属项目", "到期时间" ,"ID"}) {
            @Override
            public boolean isCellEditable(int row, int column) {
                // 设置所有单元格为不可编辑
                return false;
            }
        };
        workitemTable.setModel(baseModel);
        workitemTable.setRowHeight(30);
        workitemTable.setAutoResizeMode(0);
        // 为列表table列加上排序控件
        TableRowSorter<TableModel> sorter = new TableRowSorter<>(workitemTable.getModel());
        workitemTable.setRowSorter(sorter);

        // 设置默认的排序顺序为升序
        List<RowSorter.SortKey> sortKeys = new ArrayList<>();
        int columnIndexToSort = 0;
        sortKeys.add(new RowSorter.SortKey(columnIndexToSort, SortOrder.ASCENDING));
        sorter.setSortKeys(sortKeys);

        // 第一列默认图像加载器
        DefaultTableCellRenderer imageRenderer = new DefaultTableCellRenderer() {
            @Override
            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
                // 根据表格中对应单元格的值来动态加载图片
                if (value != null) {
                    String imageUrl = (String) value;
                    // 创建一个image对象
                    URL url = this.getClass().getResource(imageUrl);
                    if (url!=null){
                        ImageIcon imageIcon = new ImageIcon(url);
                        this.setIcon(imageIcon);
                    }
                    return this;
                } else {
                    return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
                }
            }
        };

        // 将第一列(索引为0)的渲染器设置为自定义的图片渲染器
        workitemTable.getColumnModel().getColumn(0).setCellRenderer(imageRenderer);

        // 设置列宽
        int[] columnsWidth = {
                30, 120, 400, 80, 80, 100, 150, 0
        };
        // 配置表的列宽。
        int i = 0;
        for (int width : columnsWidth) {
            TableColumn column = workitemTable.getColumnModel().getColumn(i++);
            column.setMinWidth(width);
            column.setPreferredWidth(width);
            column.setResizable(true);
        }

    }

    // 添加监听器
    private void addListeners() {
        // 添加工作项检索栏的监听
        searchField.addKeyListener(new KeyListener() {
            @Override
            public void keyTyped(KeyEvent e) {
            }

            @Override
            public void keyPressed(KeyEvent e) {
            }

            @Override
            public void keyReleased(KeyEvent e) {
                // 键盘释放时触发
                String searchText = searchField.getText();
                // 添加列表过滤
                TableRowSorter<DefaultTableModel> sorter = new TableRowSorter<>(baseModel);
                workitemTable.setRowSorter(sorter);
                // 过滤工作项key和title
                RowFilter<DefaultTableModel, Integer> workitemKeyFilter = RowFilter.regexFilter("(?i).*" + searchText + ".*", 1);
                RowFilter<DefaultTableModel, Integer> titleFilter = RowFilter.regexFilter("(?i).*" + searchText + ".*", 2);
                Iterable<RowFilter<DefaultTableModel, Integer>> filters = Arrays.asList(workitemKeyFilter, titleFilter);
                // 更新内容
                sorter.setRowFilter(RowFilter.orFilter(filters));
            }
        });
        // 项目选择框监听
        projectCombo.addItemListener(new ItemListener() {
            @Override
            public void itemStateChanged(ItemEvent e) {
                if (e.getStateChange() != ItemEvent.SELECTED) {
                    return;
                }
                ProjectDevops project = (ProjectDevops) e.getItem();
                buildNewInput(project);
                searchField.setText("");
            }
        });

        workitemTable.addMouseListener(new MouseListener() {
            // 工作项列表双击工作项弹出详情页面
            @Override
            public void mouseClicked(MouseEvent e) {
                if (e.getClickCount() == 2) {
                    int row = workitemTable.rowAtPoint(e.getPoint());
                    // 双击时的操作,row为双击的行的索引
                    // System.out.println("双击了第 " + row + " 行");
                    String workitemId = (String) workitemTable.getValueAt(row, 7);
                    // 获取工作项详情页面
                    WorkitemViewTable workitemViewTable = WorkitemViewTable.getInstance();
                    // 更新内容
                    workitemViewTable.refreshData(workitemId);
                    // 获取当前项目的引用
                    ToolWindowManager toolWindowManager = ToolWindowManager.getInstance(project);
                    // 获取工作项详情页面所在的工具栏窗口
                    ToolWindow workitemToolWindow = toolWindowManager.getToolWindow("Workitem");
                    if (workitemToolWindow != null) {
                        workitemToolWindow.show(); // 弹出详情界面工具栏
                    }
                }
            }

            @Override
            public void mouseEntered(MouseEvent e) {
            }

            @Override
            public void mouseExited(MouseEvent e) {
            }

            @Override
            public void mousePressed(MouseEvent e) {
            }

            @Override
            public void mouseReleased(MouseEvent e) {
            }
        });

        // 刷新按钮
        refreshButton.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                initProject(settingMap);
            }
        });
    }

    // 返回页面内容
    public JPanel getContent() {
        return contentPane;
    }
}

5、完成了上述实现后,运行项目,我们即可在左侧工具栏可以找到工作项列表视图按钮,点击刷新项目可以刷新项目和工作项列表。若工作项视图已关闭,点击[View-Tool Windows-Workitems],以此打开工作项视图,默认位于左侧工具栏,可以自由拖拽。

workItemsView

工作项列表效果如下。

workItems

按钮扩展流程说明

上文我们在工具栏添加了一个自定义的工具窗口,当我们想在其他位置,如主菜单栏新增一个自定义按钮,点击可以弹出响应窗口,可以参考如下流程。

1、在插件项目中创建一个新的 Java 类,并让它继承自 AnAction 类。例如,可以创建一个名为 MyAction 的类。
在 actionPerformed 方法中编写你的动作逻辑。这个方法会在用户执行该动作时被调用。

public class MyAction extends AnAction {  
    // 覆盖 AnAction 类的 actionPerformed 方法  
    @Override  
    public void actionPerformed(AnActionEvent e) {  
        // 在这里编写你的动作逻辑  
        // 这里可以参考上文窗口设计与实现,实现一个 WorkItemSelectDialog 类继承JDialog 然后点击按钮时展示该窗口
        WorkItemSelectDialog dialog = new WorkItemSelectDialog(e.getProject());
        // 点击按钮时展示该窗口
        dialog.show();
        // 窗口点击确认时 触发的逻辑
        if (dialog.isOK()) {
            CommitMessage messageControl = getCommitMessage(e);
            messageControl.setText(dialog.getResult());
        }
    } 

    private final CommitMessage getCommitMessage(AnActionEvent e)
    {
        Object messageControl = e.getData(VcsDataKeys.COMMIT_MESSAGE_CONTROL);
        if (messageControl instanceof CommitMessage) {
            return (CommitMessage) messageControl;
        }
        return null;
    } 
}

2、在插件的 plugin.xml 文件中注册你的 AnAction。在 <actions> 元素下添加一个新的 <group> 元素,并在其中添加一个 <add-to-group> 元素,将你的动作添加到合适的菜单或工具栏中。如下我们首先在主菜单加了个 "Sample Menu" 组,里面存放了我们的操作。

<actions>  
    <group id="MyPlugin.SampleMenu" text="Sample Menu" description="Sample menu">
      <add-to-group group-id="MainMenu" anchor="last"  />
      <action id="Myplugin.ActionDemo" class="com.primeton.devops.ide.idea.action.MyAction" text="MyAction" description="This is MyAction" />
    </group>
</actions>

效果如下: intellij-myaction-view

如果想在特定已知group-id的位置,额外添加按钮,也可以参考如下实现

<actions>  
    <action id="workItemSelectAction" class="com.primeton.devops.ide.idea.action.WorkItemSelectAction"
            text="选择工作项" description="工作项选择Action" icon="/icons/workitem.png">
        <add-to-group group-id="Vcs.MessageActionGroup" anchor="first"/>
    </action>
</actions>

# 插件打包导出

导出开发完成的插件项目

对于开发完成的项目,我们可以选中后点击 "Build->Prepare Plugin Module '项目名' For Deployment",将项目导出成Jar包

intellij-export

Jar包位于项目根目录下同名Jar包

intellij-export-jar

导入参考本文档 DevOps IDE扩展插件 -> Idea扩展插件 插件安装部分。

# 插件版本兼容

上述代码是基于IntelliJ IDEA Community Edition IC-232.9921.47(JetBrains Runtime version 17.0.8)版本开发

想兼容其他版本编译器时,推荐在对应编译器打开该插件工程,sdk选择对应的编译器版本,更新对应异常的部分,然后导出即可得到兼容对应版本插件jar。

← Eclipse插件 拉取Git代码 →