外观
差异查看器
editor.DiffView 显示两个文本之间的改动,如同 Pierre 的 diff 和代码评审工具那样:并排或上下排列,高亮行内改动的单词,折叠改动之间未变的行,新文本显示之处可以编辑。它的每一侧都是一个代码编辑器,带有编辑器的高亮、选区和命令,且只为可见之处布局和绘制。
go
import "github.com/egoist/mygo/ui/editor"
lang := editor.LanguageFor("main.go")
before, after := editor.NewBuffer(original), editor.NewBuffer(edited)
before.SetLanguage(lang)
after.SetLanguage(lang)
app.diff = &editor.DiffState{Old: before, New: after, Editable: true}
// 在视图里:
editor.DiffView(c, app.diff).Grow(1).FillWidth()DiffState 是查看器的状态,跨帧保存:它比较的缓冲区、它的选项,以及它为自己保存的内容,如展开的行和滚动位置。选项的零值即默认值,也就是 Pierre diff 的默认值。
差异跟随缓冲区:任一侧的编辑立即显示,查看器只重新比较其周围的行。New 也可以是某个编辑器的缓冲区,比如文件的缓冲区,这样差异就会随键入实时显示其改动。
布局
Layout可以在左侧显示旧文本、右侧显示新文本,两侧行对齐(DiffSplit)、同步滚动;或者只显示新文本,删除的行显示在原位置上方(DiffUnified)。Inline高亮改动行内部改了什么:按单词,并把相隔一个字符的词连起来,使一个短语读起来是一处改动(DiffInlineWordAlt,默认值)、逐个单词、从首个单词到最后一个单词、按字符,或不高亮。Indicators标记改动行的方式:在装订线边缘画竖条,删除处画条纹(DiffBars)、在其前加 + 和 −(DiffClassic),或仅靠背景色。NoBackground去掉改动行的着色,NoLineNumbers去掉行号。ContextLines是每处改动周围显示的未变行数,默认为 3。其余的行折叠成"N 行未修改"的分隔条,其按钮每次点击展开ExpansionLines行(默认 100),按住 Shift 展开全部;ExpandUnchanged、ExpandAll和CollapseAll展开或折叠全部。Wrap折行长行,两侧的各行与其对应行保持对齐;FitContent让查看器与其行等高,像在滚动视图中展示评审的文件那样。Font、LineHeight、TabSize和Theme与代码编辑器相同。改动用主题的Added、Deleted和Modified颜色着色,并在感知均匀的色彩空间中与背景混合,使它们在浅色和深色下看起来一致。
行的比较采用 git 的 histogram diff 算法,它无法拆分的部分用 Myers 算法;改动会滑动到 git 缩进启发式所指定的位置,因此新增一个函数时显示为整个函数,而不是它的最后几行加上下一个函数的开头。改动行会与最相似的行配对以便高亮单词,而不是按顺序配对。
编辑与评审
设置 Editable 后,新的一侧像代码编辑器一样编辑:输入、多个光标、撤销、查找和所有命令,差异跟随每一次键入。Cmd+F8 和 Shift+Cmd+F8 跳到下一处和上一处改动(在其他地方是 Alt+F5 和 Shift+Alt+F5),GoToHunk 跳到指定的一处。
HunkActions在指针下方或光标所在的改动上显示 Revert(撤销)和 Keep(保留):撤销把旧行放回New,保留把新行变成Old的内容,就像逐条评审 agent 的改动那样。应用也可以调用Revert和Accept做同样的事;Hunks列出各改动,Stats统计新增和删除的行数。LineSelection允许点击、拖动或 Shift+点击行号来选择行,由OnLineSelection报告,SelectedLines和SetSelectedLines读取和设置,就像评审中指向某些行的链接那样。OnGutterButton在指针所在行的装订线上显示一个 +(比如用来添加评论),点击时带着该行被调用。Annotations在行下方留出空间,放置RenderAnnotation在那里构建的内容,比如评审的评论,每条按其要求的高度显示;分栏视图的另一侧则把空间留空,使两侧行保持对齐。
go
app.diff.HunkActions = true
app.diff.OnGutterButton = func(r editor.DiffLineRange) {
app.comments = append(app.comments, comment{side: r.Side, line: r.End})
}
app.diff.Annotations = app.diff.Annotations[:0]
for i, cm := range app.comments {
app.diff.Annotations = append(app.diff.Annotations, editor.DiffAnnotation{Side: cm.side, Line: cm.line, Height: 72, Key: i})
}
app.diff.RenderAnnotation = func(c *ui.Context, a editor.DiffAnnotation) {
app.commentView(c, a.Key.(int))
}ActiveSide 是拥有键盘(或最后拥有过键盘)的一侧:在分栏视图里旧一侧获得键盘之前都是新一侧。Cursor 和 CursorPos 返回其最新光标的偏移和位置,与代码编辑器的一样;CurrentHunk 返回光标所在的改动(此时 inside 为 true),否则返回其前的最后一处,在第一处之前为 -1,计数方式如"5 处中的第 2 处";GoToHunk 移动该光标,于是"下一处改动"可以从它继续前进。Run 像其按键那样在其编辑器中运行命令,Find 带着查询打开其查找栏——在菜单先接管按键的地方,应用的菜单项就是这样做的:
go
if ui.Button(c, "Find").Clicked() {
app.diff.Run(editor.Find)
}
i, inside := app.diff.CurrentHunk()
n := len(app.diff.Hunks())
label := fmt.Sprintf("%d changes", n) // 在第一处之前
if i >= 0 {
label = fmt.Sprintf("%d of %d", i+1, n)
}
ui.Text(c, label)
if ui.Button(c, "Next change").Clicked() {
app.diff.GoToHunk(i + 1)
}
if inside && ui.Button(c, "Revert this change").Clicked() {
app.diff.Revert(i)
}样式
Theme 为差异着色,如同为代码编辑器着色一样,包括改动行。行周围的各部分由应用自己构建,如同 Pierre 的 diff 渲染它们的方式:RenderSeparator、RenderHunkActions 和 RenderGutterButton 收到查看器放置的元素以及该部分显示与操作的内容;查看器自带的外观是 DefaultSeparator、DefaultHunkActions 和 DefaultGutterButton,渲染函数可以调用它们。RenderFindBar 和 RenderGoToLine 构建两侧编辑器的栏,与代码编辑器的一样。
go
app.diff.RenderSeparator = func(c *ui.Context, sep ui.Element, s *editor.DiffSeparator) {
sep.Padding(4).Children(func() {
if s.Lines == 0 { // 补丁的一个改动块
ui.Text(c, s.Header).Font("monospace")
return
}
if ui.Button(c, fmt.Sprintf("Show %d hidden lines", s.Lines)).Clicked() {
s.ExpandAll()
}
})
}
app.diff.RenderHunkActions = func(c *ui.Context, bar ui.Element, a *editor.DiffHunkActions) {
bar.Gap(4).Children(func() {
if ui.Button(c, "Reject").Clicked() {
a.Revert()
}
if ui.PrimaryButton(c, "Accept").Clicked() {
a.Keep()
}
})
}DiffSeparator横跨查看器,分栏时横跨两侧,查看器滚动时还要减去滚动条轨道所占的空间(FitContent时没有),高 32 DIP。它折叠Lines行未变的行,ExpandUp、ExpandDown和ExpandAll分别向上、向下或全部展开,每次Step行;First和Last标记第一处改动之前和最后一处之后的分隔条,它们只朝一个方向展开。补丁的改动块对应的分隔条不含行,带有它的Header。DiffHunkActions位于指针或光标所在改动的右上方:Index和Hunk说明是哪一处,Revert和Keep执行操作。- 装订线按钮是指针所在行装订线上的按钮,点击时调用
OnGutterButton;它的渲染函数赋予它外观和标签。
补丁
editor.ParsePatch 读取统一 diff 所改动的文件,即 git diff 和 diff -u 打印的格式,包含重命名、权限模式以及新增和删除的文件;FilePatch.Apply 把一个补丁应用到旧文件的文本上。SetPatch 不需要文件本身就能显示文件的补丁,正如 PR 的 diff 到达时那样:各改动块的行按其在文件中的行号编号,每个改动块位于其头部之下。
go
files, err := editor.ParsePatch(output) // git diff 的输出
if err != nil {
return err
}
for _, f := range files {
d := &editor.DiffState{}
d.SetPatch(f)
app.files = append(app.files, d)
}无障碍
辅助技术把每一侧看作一个文本字段,"旧文本"和"新文本",旧的一侧只读,带有其文本和选区。分隔条是一个以其折叠的行命名的组,带有向上、向下或全部展开的按钮;撤销和保留是按钮,"撤销改动"和"保留改动"。这些名称以及查看器写出的文字("4 行未修改")都使用窗口的语言(MyGo 自身的字符串)。