Repository navigation
Expand file tree
/
Copy pathcode_editor.go
More file actions
276 lines (259 loc) · 9.21 KB
/
Copy pathcode_editor.go
File metadata and controls
276 lines (259 loc) · 9.21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package main
import (
"context"
"fmt"
"github.com/egoist/mygo/ui"
"github.com/egoist/mygo/ui/editor"
"github.com/CarlOpenLab/Gloss/internal/gitrepo"
)
// codeEditorState 是内置代码编辑器(整文件编辑)的短生命周期状态。
//
// 缓冲区放在这里而不是每帧新建:MyGo 把光标、选区、折叠、撤销历史和滚动
// 位置都存在 buffer/state 里,跨帧复用才留得住 —— 和冲突编辑器一个道理。
type codeEditorState struct {
on bool
loading bool
path string
err string
buffer *editor.Buffer
base *editor.Buffer // 行号槽里画改动标记的参照版本,没有基线时为 nil
code *editor.State
// img 是图片文件的预览。图片没有可编辑的文本缓冲区,这里摆位图;
// 保存/取消那一栏也不出现(看一眼就关掉,收尾动作只有关闭)。
img *ui.Bitmap
imgMeta string
}
// openCodeEditor 打开工作区里的一个文件,用 MyGo 的 editor.View 整文件编辑。
//
// 和冲突编辑器分开:那边编的是「三方版本 + 冲突标记 + 保存即 git add」,这里
// 编的是「磁盘上这一份」,保存只写工作区、不动 index。两件事的收尾动作不同,
// 混在一个 state 里迟早会有一边做错。
func (a *app) openCodeEditor(f gitrepo.File) {
if a.repoPath == "" || a.op != gitrepo.OpNone {
return
}
path := pathOf(f)
section := f.Section
image := isImagePreview(f)
a.codeEdit = codeEditorState{on: true, loading: true, path: path}
root := a.repoPath
a.run(func() {
ctx := context.Background()
repo, err := gitrepo.Open(ctx, root)
var data, base []byte
var side imageSide
if err == nil {
data, err = repo.Contents(path)
}
if err == nil && image {
// 图片没有「编辑代码」可言:摆出画面,收尾只有关闭。
side = decodeImageSide(data)
}
if err == nil && !image && looksBinary(data) {
err = fmt.Errorf("%s 不是可编辑的文本文件", path)
}
if err == nil && !image {
base = codeEditorBaseline(ctx, repo, path, section)
}
a.dispatch(func() {
s := &a.codeEdit
if !s.on || s.path != path {
return
}
s.loading = false
if err != nil {
s.err = err.Error()
return
}
if image {
if side.err != "" {
s.err = side.err
return
}
s.img, s.imgMeta = side.bitmap, side.meta
return
}
lang := editor.LanguageFor(path)
buf := editor.NewBuffer(string(data))
buf.SetLanguage(lang)
st := &editor.State{Buffer: buf, Label: "代码编辑器"}
if len(base) > 0 {
b := editor.NewBuffer(string(base))
b.SetLanguage(lang)
s.base = b
st.DiffBase = b
}
// Cmd+S 交给 Gloss 自己处理:编辑器把它认走就没机会保存了。
st.Bindings = []editor.Binding{
{Mods: ui.Cmd, Key: ui.KeyS, Command: editor.NoCommand},
}
s.buffer = buf
s.code = st
})
})
}
// codeEditorBaseline 取行号槽里画改动标记的参照版本。
//
// 取法和内置 Diff 预览一致:未暂存段对 index(:0),已暂存段对 HEAD。两处
// 用同一套「跟谁比」的规矩,同一个文件在编辑器里和在 Diff 里的增删才对得上。
// 都取不到就没有基线(未跟踪、新建的文件本来没有可比的版本)。
func codeEditorBaseline(ctx context.Context, repo *gitrepo.Repo, path string, section gitrepo.Section) []byte {
revs := []string{":0", "HEAD"}
if section == gitrepo.SectionStaged {
revs = []string{"HEAD"}
}
for _, rev := range revs {
if data, err := repo.ContentsAt(ctx, rev, path); err == nil {
return data
}
}
return nil
}
func (a *app) closeCodeEditor() { a.codeEdit = codeEditorState{} }
// closeEmbeddedEditors 收掉内置编辑器(整文件代码对话框 / 编辑器视图 /
// 改动区的 DiffView)。
//
// 换仓库时必须调:它们的缓冲区属于上一个仓库,而「保存」写的是 a.repoPath
// 底下的相对路径 —— 留着它,下一次保存就会把 A 仓的内容写进 B 仓。
func (a *app) closeEmbeddedEditors() {
a.codeEdit = codeEditorState{}
a.editorPane = editorPaneState{}
a.review = reviewState{}
// 草稿里的缓冲区属于上一个仓库,而「保存」写的是 a.repoPath 底下的
// 相对路径 —— 留着它,下一次保存就会把 A 仓的内容写进 B 仓。
a.reviewDrafts = nil
}
// codeEditorDialog 画整文件编辑器。和冲突编辑器同一副骨架(标题条 / 正文 /
// 底栏),只是正文永远是 editor.View,而不是分栏 diff。
func (a *app) codeEditorDialog(c *ui.Context, pal palette) {
if !a.codeEdit.on {
return
}
open := true
w, h := c.Size()
panelW := clamp(w-32, 640, 1180)
panelH := clamp(h-32, 420, 780)
ui.DialogBase(c, &open, func(back, panel ui.Element) {
back.Background(ui.RGBA(0, 0, 0, 0.28))
panel.Width(panelW).Height(panelH).Padding(0).Radius(radiusXL).
Background(pal.surface4).Border(1, pal.hairline).
Shadow(0, 8, 24, 0, ui.RGBA(0, 0, 0, 0.24)).Children(func() {
a.codeEditorBody(c, pal)
})
})
if !open {
a.closeCodeEditor()
}
}
func (a *app) codeEditorBody(c *ui.Context, pal palette) {
t := c.Theme()
s := &a.codeEdit
title := "编辑代码"
if s.img != nil {
title = "图片预览"
}
ui.Column(c).Fill().Children(func() {
ui.Row(c).Height(48).Shrink(0).Padding(0, 14).Gap(8).AlignItems(ui.Center).
BorderWidth(0, 0, 1, 0).BorderColor(pal.hairline).Children(func() {
ui.Column(c).Grow(1).MinWidth(0).Gap(1).Children(func() {
ui.Text(c, title).FontSize(textHeadline).FontWeight(weightSemibold).
TextColor(pal.ink)
ui.Text(c, s.path).Font("monospace").FontSize(textCaption).
SingleLine().TextColor(pal.inkMuted)
})
if s.buffer != nil && s.buffer.Modified() {
ui.Text(c, "已修改").FontSize(textCaption).TextColor(t.Warning)
}
// Cmd+S 只有落在对话框自己身上才有效:handleKeys 在 view 一开头
// 跑,而那时 c 还在对话框外面,全局快捷键在有弹窗时一律让位。
if s.code != nil && c.Shortcut(ui.Cmd, ui.KeyS) {
a.saveCodeEditor()
}
if xButton(c, pal, "关闭") {
a.closeCodeEditor()
}
})
if s.loading {
ui.Column(c).Grow(1).Center().Gap(8).Children(func() {
ui.Spinner(c)
ui.Text(c, "正在读取文件…").TextColor(t.TextMuted)
})
return
}
if s.err != "" || (s.code == nil && s.img == nil) {
ui.Column(c).Grow(1).Center().Gap(10).Padding(24).Children(func() {
headline(c, pal, "无法打开内置编辑器")
ui.Text(c, s.err).Font("monospace").FontSize(textBodySM).
TextColor(t.TextMuted)
if secondaryAction(c, pal, "关闭", "", false).Clicked() {
a.closeCodeEditor()
}
})
return
}
// 图片:摆画面,没有保存可做。看完关掉,要放大或用别的工具,
// 文件列表右键「在编辑器打开」交给系统看图应用。
if s.img != nil {
imagePanel(c, pal, "", imageSide{bitmap: s.img, meta: s.imgMeta})
ui.Row(c).Height(48).Shrink(0).Padding(0, 12).Gap(8).AlignItems(ui.Center).
BorderWidth(1, 0, 0, 0).BorderColor(pal.hairline).Children(func() {
ui.Text(c, "图片不能在这里编辑;右键「在编辑器打开」可用系统看图应用查看原图").
FontSize(textCaption).TextColor(pal.inkMuted).Grow(1).MinWidth(0)
if secondaryAction(c, pal, "关闭", "", false).Clicked() {
a.closeCodeEditor()
}
})
return
}
ui.Box(c).Grow(1).MinHeight(0).FillWidth().Children(func() {
th := editor.DefaultTheme(t)
th.Background = pal.codeBg
th.Text = pal.code
s.code.Theme = th
s.code.Font = ui.Font{Family: "monospace", Size: a.codeSizeValue()}
editor.View(c, s.code).Fill().AutoFocus()
})
ui.Row(c).Height(48).Shrink(0).Padding(0, 12).Gap(8).AlignItems(ui.Center).
BorderWidth(1, 0, 0, 0).BorderColor(pal.hairline).Children(func() {
hint := "保存只写工作区,不会替你暂存 —— 改动会出现在「未暂存」里"
if s.base != nil {
hint += ";行号槽的标记是相对暂存区/HEAD 的改动"
}
ui.Text(c, hint).FontSize(textCaption).TextColor(pal.inkMuted).
Grow(1).MinWidth(0)
busy := a.op != gitrepo.OpNone
if secondaryAction(c, pal, "取消", "放弃本次编辑", busy).Clicked() {
a.closeCodeEditor()
}
if primaryAction(c, pal, "保存", "写入工作区(⌘S)", busy).Clicked() {
a.saveCodeEditor()
}
})
})
}
// saveCodeEditor 把缓冲区写回工作区。
//
// 只走 WriteContents:不 git add、不碰 index。用户按下「保存」想要的是磁盘上
// 那一份变了,暂存与否是下一步自己的决定(暂存区的勾选框就在文件行左边)。
func (a *app) saveCodeEditor() {
s := &a.codeEdit
if !s.on || s.buffer == nil || a.op != gitrepo.OpNone {
return
}
path := s.path
text := s.buffer.String()
buf := s.buffer
a.startOp(gitrepo.OpWrite, "正在保存 "+path, func(ctx context.Context, repo *gitrepo.Repo) (string, error) {
if err := repo.WriteContents(path, []byte(text)); err != nil {
return "", err
}
return "已保存 " + path, nil
}, func(ok bool) {
if ok && a.codeEdit.on && a.codeEdit.buffer == buf {
// 行号槽的基线也跟着走:刚写进去的这份就是新的工作区内容,
// 而基线上一步已经在 reloadAfterOp 里重读过了。
a.closeCodeEditor()
}
a.reloadAfterOp()
})
}