前言
众所周知,程序员最讨厌的四件事:写注释、写文档、别人不写注释、别人不写文档。因此,想办法降低文档的编写和维护成本是很有必要的。当前写技术文档的模式如图:

痛点总结有如下三方面:

针对上述问题,我们的解决思路:
本地的编辑、浏览工作收敛至 IDE,提供沉浸式体验;
在文档、代码间建立强关联,减少拷贝,提升联动性,同时提升文档的触达率;
代码与文档同属一个 Git 仓库,借助版本管理,避免因业务迭代导致的文档版本与代码不匹配;
制作可将文档导出到线上的工具,可利用浏览器做到随时访问;
方案总览

与原始模式相比,新方案可以做到完全脱离浏览器 / 文档编辑器,线上页面的同步完全交给定时触发的自动化部署。
图中橙色部分是方案的重点,按照分工,划分为线下、线上两部分,职责如下:
线下:IDEA Plugin
实现自定义语言的解析、分析;
提供文档内容的预览器、编辑器;
提供一系列实用功能,关联代码与文档;
线上:Gradle / Dokka Plugin
桥接、复用 IDE Plugin 的语义分析、预览内容生成能力;
扩展 Dokka Renderer,实现 HTML 与飞书文档的导出能力;
方案建设使用了不少有意思的技术,放到后面详细介绍。
线下效果
IDEA Plugin 提供一个侧边栏和强大的编辑器。下面分别从编辑、浏览两个角度介绍。
编辑体验
假设存在源码如下:
public class ClassA {
public static final String TAG = "tag";
ClassB b;
/**
* method document here.
*
* @param params input string
*/
public static void invoke(@NotNull String params) {
System.out.println("invoke method!");
System.out.println("this is method body: " + params);
}
public ClassA() {
System.out.println("create new instance!");
}
private static final class ChildClass {
/**
* This is a method from inner class.
*/
void innerInvoke() {
System.out.println("invoke method from child!");
}
}
}
文档中添加该类的引用就是这个效果:
不同于复制、粘贴代码,新方案有如下优势:
关联性更强,预览会随代码片段的变更时时改变;
易于重构,被引用的类名、方法名、字段名发生重命名时,文档内容会自动随之变化,防止引用失效;
更加直观,编辑、浏览时能更快速地找到代码出处;
输入更流畅,有完善的补全能力;
浏览体验
相对于普通 Markdown,新方案用起来更加友善:
沉浸式使用,界面内嵌在 IDE 内,无需跳转到其他应用;
被提及的源码旁均有行标,点击一键查阅文档;
文档“浏览器”支持与 IDE 一致的代码高亮、引用跳转;
线上效果
代码中文档会定期自动部署到远端。以一篇真实业务文档举例,HTML 部署到轻服务后长这样:

对应飞书的产物长这样:

这些线上页面主要面向非当前团队的读者,内容由 CI 定时同步,暂不提供跳转到 IDE 的能力。
技术实现
项目的架构如图所示:

考虑到用户体验部分主要在 IDEA(Android Studio)内呈现,我们的技术栈选择基于 IntelliJ 打造。按模块可分为三部分:
基建层
IDEA Plugin
Gradle / Dokka Plugin
通用逻辑(语言实现相关)封装在基建层,仅依赖 IntelliJ Core。相对于 IntelliJ Platform,IntelliJ Core 仅保留语言相关的能力,精简了 codeInsight、UI 组件等代码,被广泛用于 IntelliJ 各大产品中(包括图中的 Kotlin、Dokka 等)。
下面将针对这三个主要模块展开介绍。
基建
纵观整个方案,基建层是所有功能的基石,其最核心的能力是建立代码与文档关联。这里我们设计实现了一套标记语言 CodeRef,满足以下几个需求:
语法简洁,结构上与源码一一对应;
指向精准,即必须满足一对一的关系;
支持仅保留声明(去掉 body),提升信噪比;
有扩展性,方便后续迭代新功能;
CodeRef 语言并不复杂,采用类似 Kotlin/Java 的风格,用关键字、字符串、括号构成语句和代码块,代码块中每个节点都有与之对应的源码节点。下图是一个简单的示例,对应关系用着色文字标识: