从0到1:JetBrains 插件开发系列(八 · 终章)实战项目:打造智能 TODO 管理
从0到1:JetBrains 插件开发系列(八 · 终章)
实战项目:打造智能 TODO 管理插件
作者:于天惠
在日常编码中,我们常写下这样的注释:
// TODO: 优化性能// FIXME: 这里可能有空指针// HACK: 临时绕过权限校验
但这些 TODO 往往散落在成百上千个文件中,难以追踪、容易遗忘。
今天,我们要开发一款插件:SmartTODO —— 它能:
-
• 📌 自动扫描项目中所有 TODO/FIXME/HACK注释 -
• 🎨 在编辑器侧边栏高亮显示 -
• 🔍 提供全局面板集中管理 -
• ✅ 支持标记为“已处理”并自动归档 -
• ⚙️ 允许自定义关键词与颜色
这不仅是一个功能集合,更是一次工程化插件开发的完整演练。
一、架构设计:模块划分
我们将插件拆分为四个核心模块:
|
|
|
|
|---|---|---|
| Scanner |
|
|
| Model |
|
|
| UI |
|
|
| Action |
|
|
💡 设计原则:低耦合、可测试、可扩展
二、核心实现 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 中添加配置面板:
-
• 允许用户添加/删除关键词(如 NOTE,REVIEW) -
• 为每个关键词设置高亮颜色
通过 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 更好用一点”的念头。而今天,你拥有了将念头变为现实的能力。
愿你的插件,成为他人开发路上的一盏灯。
感谢陪伴,系列完结。但你的开发之旅,才刚刚开始。

夜雨聆风
