乐于分享
好东西不私藏

从0到1:JetBrains 插件开发系列(八 · 终章)实战项目:打造智能 TODO 管理

从0到1:JetBrains 插件开发系列(八 · 终章)实战项目:打造智能 TODO 管理

从0到1:JetBrains 插件开发系列(八 · 终章)

实战项目:打造智能 TODO 管理插件

作者:于天惠


在日常编码中,我们常写下这样的注释:

// TODO: 优化性能// FIXME: 这里可能有空指针// HACK: 临时绕过权限校验

但这些 TODO 往往散落在成百上千个文件中,难以追踪、容易遗忘

今天,我们要开发一款插件:SmartTODO —— 它能:

  • • 📌 自动扫描项目中所有 TODO / FIXME / HACK 注释
  • • 🎨 在编辑器侧边栏高亮显示
  • • 🔍 提供全局面板集中管理
  • • ✅ 支持标记为“已处理”并自动归档
  • • ⚙️ 允许自定义关键词与颜色

这不仅是一个功能集合,更是一次工程化插件开发的完整演练


一、架构设计:模块划分

我们将插件拆分为四个核心模块:

模块
职责
使用技术
Scanner
扫描 PSI 中的注释并提取 TODO 项
PSI Visitor + Background Task
Model
存储 TODO 列表与状态(未处理/已处理)
PersistentStateComponent
UI
侧边栏高亮 + 全局工具窗口
EditorGutterRenderer + ToolWindow
Action
提供“标记完成”、“跳转到位置”等操作
AnAction + QuickFix

💡 设计原则:低耦合、可测试、可扩展


二、核心实现 1:扫描 TODO 注释(PSI + 后台任务)

步骤 1:定义 TODO 数据结构

data class TodoItem(    val text: String,    val keyword: String, // "TODO", "FIXME" 等    val file: VirtualFile,    val offset: Int,    var isDone: Boolean = false)

步骤 2:实现 PSI 扫描器

class TodoScanner {fun scan(psiFile: PsiFile): List<TodoItem> {        val items = mutableListOf<TodoItem>()        psiFile.accept(object : PsiRecursiveElementVisitor() {            overridefun visitComment(comment: PsiComment) {                val text = comment.text                for (keyword in TodoSettings.instance.keywords) {                    if (text.contains("$keyword:", ignoreCase = true)) {                        items.add(TodoItem(                            text = text.trim(),                            keyword = keyword,                            file = psiFile.virtualFile!!,                            offset = comment.textOffset                        ))                    }                }                super.visitComment(comment)            }        })        return items    }}

步骤 3:在后台扫描整个项目

class ScanAllTodosTask(private val project: Project) : Task.Backgroundable(project, "Scanning TODOs...", true) {    overridefun run(indicator: ProgressIndicator) {        val model = TodoModel.getInstance(project)        model.clear()        val files = ProjectRootManager.getInstance(project).contentSourceRoots            .flatMap { VfsUtil.collectChildrenRecursively(it) }            .filter { it.extension in listOf("java", "kt", "py", "js") }        indicator.fraction = 0.0        files.forEachIndexed { index, file ->            indicator.checkCanceled()            indicator.text = "Scanning ${file.name}"            val psiFile = PsiManager.getInstance(project).findFile(file) ?: return@forEachIndexed            model.addAll(TodoScanner().scan(psiFile))            indicator.fraction = index.toDouble() / files.size        }    }    overridefun onSuccess() {        // 刷新 UI        TodoToolWindow.refresh(project)    }}

✅ 关键点:使用 Task.Backgroundable 避免阻塞 UI,进度条提升体验。


三、核心实现 2:持久化与状态管理

定义状态模型

@State(name = "SmartTODO", storages = [Storage("smart-todo.xml")])@Service(Service.Level.PROJECT)class TodoModel(private val project: Project) : PersistentStateComponent<TodoModel> {    private val _items = mutableListOf<TodoItem>()    val items: List<TodoItem> get() = _items.toList()fun addAll(items: List<TodoItem>) {        _items.addAll(items)    }fun markAsDone(item: TodoItem) {        item.isDone = true        // 自动保存(因是 Service)    }    overridefun getState() = this    overridefun loadState(state: TodoModel) {        XmlSerializerUtil.copyBean(state, this)    }}

🔄 注意:因为是 PROJECT 级 Service,每个项目有独立的 TODO 列表。


四、核心实现 3:UI 展示

1. 编辑器侧边栏高亮(Gutter Renderer)

class TodoGutterRenderer(private val todoItem: TodoItem) : RelatedItemLineMarkerProvider() {    overridefun collectNavigationMarkers(        element: PsiElement,        result: MutableCollection<in RelatedItemLineMarkerInfo<*>>    ) {        if (element is PsiComment && element.textOffset == todoItem.offset) {            val icon = AllIcons.General.Todo            val marker = NavigationGutterIconBuilder                .create(icon)                .setTooltipText(todoItem.text)                .setPopupTitle("TODO Actions")                .setAlignment(GutterIconRenderer.Alignment.RIGHT)                .createLineMarkerInfo(element)            result.add(marker)        }    }}

并在 plugin.xml 中注册:

<codeVisionProvider language="JAVA" implementationClass="ui.TodoGutterRenderer"/><!-- 或使用 lineMarkerProvider(旧版 API) -->

2. 全局工具窗口(ToolWindow)

创建 TodoToolWindowFactory

class TodoToolWindowFactory : ToolWindowFactory {    overridefun createToolWindowContent(project: Project, toolWindow: ToolWindow) {        val panel = JBPanel<JBPanel<*>>(BorderLayout())        val table = createTodoTable(project)        panel.add(JBScrollPane(table), BorderLayout.CENTER)        toolWindow.contentManager.addContent(toolWindow.contentManager.factory.createContent(panel, "", false))    }    privatefun createTodoTable(project: Project): TableView<TodoItem> {        val model = ListTableModel<TodoItem>(            arrayOf(                ColumnInfo("Keyword", TodoItem::keyword),                ColumnInfo("Text", TodoItem::text),                ColumnInfo("File", { FileUtil.getRelativePath(project.basePath, it.file.path) ?: it.file.name })            ),            TodoModel.getInstance(project).items.filterNot { it.isDone }        )        return TableView(model)    }}

注册到 plugin.xml

<toolWindow id="SmartTODO" factoryClass="ui.TodoToolWindowFactory" anchor="right"/>

🎯 效果:右侧工具窗口列出所有未完成 TODO,点击可跳转。


五、核心实现 4:用户操作(Action + QuickFix)

1. 右键菜单:“Mark as Done”

class MarkTodoAsDoneAction : AnAction("Mark as Done") {    overridefun update(e: AnActionEvent) {        e.presentation.isEnabled = e.getData(TODO_ITEM_DATA_KEY) != null    }    overridefun actionPerformed(e: AnActionEvent) {        val item = e.getData(TODO_ITEM_DATA_KEY) ?: return        TodoModel.getInstance(e.project!!).markAsDone(item)        TodoToolWindow.refresh(e.project!!)    }}

2. 快速修复(QuickFix):在注释旁提供“✓ Done”选项

class MarkTodoDoneQuickFix(private val item: TodoItem) : IntentionAction {    overridefun getText() = "✓ Mark TODO as done"    overridefun isAvailable(project: Project, editor: Editor?, file: PsiFile?) = true    overridefun invoke(project: Project, editor: Editor?, file: PsiFile?) {        TodoModel.getInstance(project).markAsDone(item)        TodoToolWindow.refresh(project)    }}

并在 Inspection 中注册该 QuickFix。


六、配置支持:自定义关键词与颜色

在 Settings 中添加配置面板:

  • • 允许用户添加/删除关键词(如 NOTEREVIEW
  • • 为每个关键词设置高亮颜色

通过 PersistentStateComponent 保存 List<TodoKeywordConfig>,并在扫描和渲染时读取。

🎨 提示:使用 ColorPicker 组件让用户直观选择颜色。


七、测试与发布

1. 编写 Light Test

fun `test scanner extracts TODO from Java comment`() {    myFixture.configureByText("App.java", "// TODO: fix this\npublic class App {}")    val items = TodoScanner().scan(myFixture.file)    assertEquals(1, items.size)    assertEquals("TODO", items[0].keyword)}

2. 打包与发布

  • • 执行 ./gradlew buildPlugin
  • • 上传 ZIP 到 plugins.jetbrains.com
  • • 插件 ID:com.yourname.smarttodo

八、延伸思考:还能做什么?

这个项目只是起点,你可以继续增强:

  • • 📅 添加“截止日期”字段,集成日历提醒
  • • 🤖 使用 LLM 自动生成 TODO 的解决方案建议
  • • 🌐 同步 TODO 到 Jira/Trello(通过 REST API)
  • • 📊 生成项目 TODO 报告(按模块/严重性统计)

终章结语:你已具备创造能力

从第一行 AnAction,到今天的完整产品,你已经走过了 JetBrains 插件开发的完整生命周期

  • • 响应用户(Action)
  • • 理解代码(PSI)
  • • 构建界面(UI)
  • • 管理状态(Service)
  • • 保障质量(Test)
  • • 触达用户(Publish)

现在,你不再只是使用者,而是创造者。

JetBrains 生态中有超过 7000 个插件,其中许多最初都只是一个“我想让 IDE 更好用一点”的念头。而今天,你拥有了将念头变为现实的能力。

愿你的插件,成为他人开发路上的一盏灯。

感谢陪伴,系列完结。但你的开发之旅,才刚刚开始。


本站文章均为手工撰写未经允许谢绝转载:夜雨聆风 » 从0到1:JetBrains 插件开发系列(八 · 终章)实战项目:打造智能 TODO 管理

评论 抢沙发

1 + 9 =
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址
×
订阅图标按钮