@young1lin/dsh-ui-gitworkbench 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/CHANGELOG_EN.md +43 -0
- package/lib/apply-blocks.js +159 -0
- package/lib/atomic-json.js +23 -5
- package/lib/blame.js +83 -0
- package/lib/client.js +34960 -11826
- package/lib/git-ops.js +25 -0
- package/lib/image-sniff.js +197 -0
- package/lib/index.js +401 -7
- package/lib/patch-model.js +223 -0
- package/lib/side-guard.js +55 -0
- package/lib/write-checked.js +164 -0
- package/package.json +7 -1
- package/src/apply-blocks.ts +215 -0
- package/src/atomic-json.ts +29 -5
- package/src/blame.ts +94 -0
- package/src/client/CodeEditor.tsx +317 -0
- package/src/client/FileBrowser.tsx +657 -0
- package/src/client/GitWorkbenchPanel.module.css +491 -12
- package/src/client/GitWorkbenchPanel.tsx +1655 -190
- package/src/client/ImageView.tsx +120 -0
- package/src/client/blame-gutter.ts +108 -0
- package/src/client/blame-view.ts +104 -0
- package/src/client/cm-diff.ts +108 -0
- package/src/client/cm-tokens.ts +79 -0
- package/src/client/diff-nav.ts +198 -0
- package/src/client/file-icon.ts +190 -0
- package/src/client/file-rows.ts +184 -0
- package/src/client/files-place.ts +178 -0
- package/src/client/glyphs.tsx +86 -0
- package/src/client/highlight.ts +25 -0
- package/src/client/history-layout.ts +52 -0
- package/src/client/idle-value.ts +53 -0
- package/src/client/image-view.ts +106 -0
- package/src/client/indent.ts +74 -0
- package/src/client/index.ts +59 -0
- package/src/client/locales.ts +179 -4
- package/src/client/pane-size.ts +71 -0
- package/src/client/side-edit.ts +244 -0
- package/src/client/side-rows.ts +258 -0
- package/src/client/stable-list.ts +31 -0
- package/src/client/use-change-nav.ts +83 -0
- package/src/client/worktree-view.ts +11 -1
- package/src/git-ops.ts +36 -1
- package/src/image-sniff.ts +204 -0
- package/src/index.ts +447 -7
- package/src/patch-model.ts +267 -0
- package/src/side-guard.ts +58 -0
- package/src/write-checked.ts +223 -0
package/src/client/locales.ts
CHANGED
|
@@ -22,10 +22,18 @@ export type WorkbenchKey =
|
|
|
22
22
|
| 'aheadTitle' | 'behindTitle' | 'files'
|
|
23
23
|
| 'filterFiles' | 'filterFilesPlaceholder' | 'filterFilesClear' | 'filesFiltered' | 'filterNoMatch'
|
|
24
24
|
| 'drawerLabel' | 'totalsDim' | 'refresh' | 'close'
|
|
25
|
-
| 'tabsLabel' | 'tabChanges' | 'tabHistory' | 'tabCompare'
|
|
25
|
+
| 'tabsLabel' | 'tabChanges' | 'tabHistory' | 'tabCompare' | 'tabFiles'
|
|
26
|
+
// the Files tab: browse the repository, read a file, blame it, edit it
|
|
27
|
+
| 'fileSearchPlaceholder' | 'filesTruncated' | 'filesEmpty' | 'filesNoMatch' | 'filesPick'
|
|
28
|
+
| 'filesUnsavedAsk' | 'filesDiscardOpen' | 'filesMore' | 'filesVanished' | 'fileReadOnlyCrlf' | 'fileReadOnlyEncoding'
|
|
29
|
+
| 'blameWhileEditing' | 'blameLine' | 'blamePick' | 'blameInHistory' | 'paintTooLarge'
|
|
30
|
+
| 'imageBroken' | 'imageFit' | 'imageActual' | 'imageTooLarge' | 'imageSource' | 'imagePreview'
|
|
31
|
+
| 'prevChange' | 'nextChange' | 'prevChangeHint' | 'nextChangeHint' | 'changeCount'
|
|
26
32
|
| 'sourceLabel' | 'workingTree'
|
|
27
33
|
| 'loadingCommit' | 'renamedFrom' | 'binaryFile' | 'loadingDiff' | 'noTextDiff'
|
|
28
34
|
| 'noCommits' | 'historyLabel' | 'historyEnd' | 'loading' | 'maximize' | 'restore'
|
|
35
|
+
// side-by-side diff pane: the two layer tabs, and the size-guard notice
|
|
36
|
+
| 'tabUnstaged' | 'tabStaged' | 'diffTooLarge'
|
|
29
37
|
| 'commitAuthor' | 'commitCommitter' | 'commitDate' | 'historyFilterPlaceholder' | 'historyNoMatch'
|
|
30
38
|
| 'filterClearAll' | 'filterBy' | 'filterUsers' | 'filterUserSearch' | 'filterAuthorsMore'
|
|
31
39
|
| 'filterDate' | 'filterToday' | 'filterLast7' | 'filterLast30' | 'filterAfter' | 'filterBefore'
|
|
@@ -38,7 +46,9 @@ export type WorkbenchKey =
|
|
|
38
46
|
| 'scopeProject' | 'scopeGlobal' | 'scopeGlobalHint' | 'scopeNoRepo'
|
|
39
47
|
| 'bgNone' | 'bgChoose' | 'bgClear' | 'bgBlur' | 'bgVeil' | 'bgWorking' | 'bgFailed' | 'bgTooBig'
|
|
40
48
|
| 'cssPlaceholder' | 'cssImport' | 'cssApply' | 'cssUnapplied' | 'styleFailed'
|
|
41
|
-
| 'resizeLabel' | 'resizeCommits' | 'resizeTree'
|
|
49
|
+
| 'resizeLabel' | 'resizeCommits' | 'resizeTree' | 'resizeSides'
|
|
50
|
+
// History arrangement switch: the commit list beside the diff, or above it
|
|
51
|
+
| 'historyLayout' | 'layoutColumns' | 'layoutStacked'
|
|
42
52
|
| 'expandAll' | 'collapseAll' | 'noBranch'
|
|
43
53
|
| 'copyCommit' | 'copiedCommit'
|
|
44
54
|
// write operations
|
|
@@ -48,11 +58,22 @@ export type WorkbenchKey =
|
|
|
48
58
|
| 'stage' | 'unstage' | 'stageAll' | 'unstageAll' | 'stagedCount'
|
|
49
59
|
| 'commit' | 'amend' | 'commitPlaceholder' | 'commitNeedMessage' | 'commitLead'
|
|
50
60
|
| 'op.ok.stage' | 'op.ok.unstage' | 'op.ok.commit' | 'op.ok.fetch' | 'op.ok.pull' | 'op.ok.push'
|
|
51
|
-
| 'op.ok.discardFile'
|
|
61
|
+
| 'op.ok.discardFile' | 'op.ok.applyBlocks'
|
|
52
62
|
| 'discardAction' | 'discardTitle' | 'discardConfirm' | 'discardCancel'
|
|
53
63
|
| 'discardBodyRestore' | 'discardBodyDelete' | 'discardBodyUnrename'
|
|
64
|
+
// side-by-side block actions: the three buttons and the roll-back confirmation's wording
|
|
65
|
+
| 'blockStage' | 'blockDiscard' | 'blockUnstage' | 'blockDiscardBody' | 'blockDiscardBodyDelete'
|
|
66
|
+
// side-by-side editing: arm the editor, save, revert, the stale/conflict
|
|
67
|
+
// banner, the CRLF refusal notice, and the unsaved-changes prompt that
|
|
68
|
+
// guards every gesture dropping the buffer (tab, file, close)
|
|
69
|
+
| 'editFile' | 'fileSave' | 'fileRevert' | 'editingNotice' | 'crlfNotice' | 'encodingNotice'
|
|
70
|
+
// blame gutter on the working-tree column
|
|
71
|
+
| 'blameToggle' | 'blameHint' | 'blameUncommitted' | 'blameFailed' | 'blameTruncated'
|
|
72
|
+
| 'saveFailed' | 'saveUnavailable' | 'saveRetry'
|
|
73
|
+
| 'staleTitle' | 'staleBody' | 'staleReload' | 'staleOverwrite'
|
|
74
|
+
| 'unsavedTitle' | 'unsavedBody' | 'unsavedLeave' | 'unsavedStay'
|
|
54
75
|
| 'op.fail.auth' | 'op.fail.network' | 'op.fail.no-upstream' | 'op.fail.diverged' | 'op.fail.conflict'
|
|
55
|
-
| 'op.fail.nothing-to-commit' | 'op.fail.dirty' | 'op.fail.unknown'
|
|
76
|
+
| 'op.fail.nothing-to-commit' | 'op.fail.dirty' | 'op.fail.stale' | 'op.fail.invalid' | 'op.fail.unknown'
|
|
56
77
|
|
|
57
78
|
export const zh: Record<WorkbenchKey, string> = {
|
|
58
79
|
aheadTitle: '领先上游 {count} 个提交',
|
|
@@ -66,6 +87,25 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
66
87
|
tabChanges: '变更',
|
|
67
88
|
tabHistory: '历史',
|
|
68
89
|
tabCompare: '对比',
|
|
90
|
+
tabFiles: '文件',
|
|
91
|
+
fileSearchPlaceholder: '搜索文件…',
|
|
92
|
+
filesTruncated: '文件太多,列表已截断;用搜索找剩下的。',
|
|
93
|
+
filesEmpty: '这个仓库还没有文件',
|
|
94
|
+
filesNoMatch: '没有匹配的文件',
|
|
95
|
+
filesMore: '还有 {count} 个,用上面的搜索找',
|
|
96
|
+
filesPick: '从左边选一个文件打开',
|
|
97
|
+
filesVanished: '{path} 已不在仓库里(被删除、重命名,或者切到了没有它的分支)。',
|
|
98
|
+
filesUnsavedAsk: '有未保存的修改,打开其他文件会丢弃它们。',
|
|
99
|
+
filesDiscardOpen: '丢弃并打开',
|
|
100
|
+
// Read-only rather than withheld: in this view READING the file is the
|
|
101
|
+
// point, and only saving it back would rewrite bytes nobody touched.
|
|
102
|
+
fileReadOnlyCrlf: '这个文件的行尾是 CRLF,只能查看不能编辑(编辑器会把行尾统一成 LF,保存时整份文件都会被改写)。',
|
|
103
|
+
fileReadOnlyEncoding: '这个文件不是 UTF-8 编码,只能查看不能编辑:页面上的文字是一次有损解码,保存回去会改写每一个非 ASCII 字节。',
|
|
104
|
+
blameLine: '第 {line} 行',
|
|
105
|
+
paintTooLarge: '文件较长,已关闭语法上色(整文件上色每次要一秒以上,每次停手都卡一下比纯文本更难用)。编辑、保存、追溯不受影响。',
|
|
106
|
+
blameInHistory: '他在本文件的提交',
|
|
107
|
+
blamePick: '点左边的名字,看那一行是哪个提交改的',
|
|
108
|
+
blameWhileEditing: '正在编辑,追溯已暂时收起:改过的行号已经对不上它背后的提交。保存或放弃后会重新出现。',
|
|
69
109
|
historyEnd: '已到最早的提交',
|
|
70
110
|
loading: '加载中…',
|
|
71
111
|
maximize: '最大化',
|
|
@@ -111,14 +151,34 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
111
151
|
styleFailed: '保存失败',
|
|
112
152
|
resizeLabel: '拖动调整抽屉宽度',
|
|
113
153
|
resizeCommits: '拖动调整提交列表宽度',
|
|
154
|
+
historyLayout: '提交列表布局',
|
|
155
|
+
layoutColumns: '并排:列表与差异分列',
|
|
156
|
+
layoutStacked: '上下:列表横跨顶部',
|
|
114
157
|
resizeTree: '拖动调整文件树宽度',
|
|
158
|
+
resizeSides: '拖动调整左右两栏宽度',
|
|
115
159
|
sourceLabel: '统计来源切换',
|
|
116
160
|
workingTree: '工作区',
|
|
117
161
|
loadingCommit: '加载提交…',
|
|
118
162
|
renamedFrom: '重命名自',
|
|
119
163
|
binaryFile: '二进制文件 —— 不显示文本 diff',
|
|
164
|
+
imageBroken: '浏览器无法解码这个图片文件(文件头对得上,但内容可能已损坏或被截断)。',
|
|
165
|
+
imageFit: '适应窗口',
|
|
166
|
+
imageActual: '实际大小',
|
|
167
|
+
imageTooLarge: '图片有 {size},超过 {cap} 的预览上限。',
|
|
168
|
+
imageSource: '看源码',
|
|
169
|
+
imagePreview: '看图',
|
|
170
|
+
prevChange: '上一处变更',
|
|
171
|
+
nextChange: '下一处变更',
|
|
172
|
+
prevChangeHint: '上一处变更(Shift+F7)',
|
|
173
|
+
nextChangeHint: '下一处变更(F7)',
|
|
174
|
+
changeCount: '变更 {n} 处',
|
|
120
175
|
loadingDiff: '加载 diff…',
|
|
121
176
|
noTextDiff: '无文本差异',
|
|
177
|
+
// The side-by-side pane's layer tabs: unstaged is index→worktree, staged is
|
|
178
|
+
// HEAD→index — the two halves of what the old combined view showed at once.
|
|
179
|
+
tabUnstaged: '未暂存',
|
|
180
|
+
tabStaged: '已暂存',
|
|
181
|
+
diffTooLarge: '文件过大(超过 20000 行或 2 MB),已改用普通 diff 视图',
|
|
122
182
|
noCommits: '无提交历史',
|
|
123
183
|
historyLabel: '提交历史',
|
|
124
184
|
commitAuthor: '作者',
|
|
@@ -193,6 +253,7 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
193
253
|
'op.ok.pull': '拉取完成',
|
|
194
254
|
'op.ok.push': '推送成功',
|
|
195
255
|
'op.ok.discardFile': '已撤回',
|
|
256
|
+
'op.ok.applyBlocks': '已应用',
|
|
196
257
|
// The row action, and the dialog it opens. IDEA calls this Rollback and
|
|
197
258
|
// means "take the file back to its committed state" — not "undo my last
|
|
198
259
|
// edit", which is the editor's job and a different promise.
|
|
@@ -205,6 +266,43 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
205
266
|
discardBodyRestore: '{path} 将还原成上次提交时的样子。这里的 {added} 行新增、{deleted} 行删除无法找回。',
|
|
206
267
|
discardBodyDelete: '{path} 从未被 git 记录过,删除后无法找回。',
|
|
207
268
|
discardBodyUnrename: '撤销重命名:{path} 改回 {previousPath},改名期间的内容改动一并丢弃。',
|
|
269
|
+
// One BLOCK, not the whole file: the side pane's roll-back states exactly
|
|
270
|
+
// which lines leave and that the working-tree file is rewritten to do it.
|
|
271
|
+
blockStage: '暂存这块',
|
|
272
|
+
blockDiscard: '撤回这块',
|
|
273
|
+
blockUnstage: '取消暂存这块',
|
|
274
|
+
blockDiscardBody: '{path} 的这一块改动({added} 行新增、{deleted} 行删除)将被撤回,工作区文件随之改写,无法找回。',
|
|
275
|
+
// The untracked case: the file's whole content is the one block, so rolling
|
|
276
|
+
// the block back reverse-applies the new-file patch and DELETES the file —
|
|
277
|
+
// the same consequence class the file-level delete wording exists to name.
|
|
278
|
+
blockDiscardBodyDelete: '{path} 从未被 git 记录过,这一块就是整个文件,撤回后文件将被删除,无法找回。',
|
|
279
|
+
// The editable right column: editing arms explicitly (never per keystroke —
|
|
280
|
+
// an autosave racing a concurrent agent write is the data-loss case), and
|
|
281
|
+
// every save carries the sha the editor opened with.
|
|
282
|
+
editFile: '编辑',
|
|
283
|
+
fileSave: '保存(Ctrl/Cmd+S)',
|
|
284
|
+
fileRevert: '放弃修改',
|
|
285
|
+
editingNotice: '正在编辑且未保存:保存前区块操作不可用;切走前(换页签、选其他文件、关闭抽屉)会先确认。',
|
|
286
|
+
// Why the edit affordance is withheld on a CRLF file: the editor control
|
|
287
|
+
// itself rewrites \r\n to \n, so any save would change every line ending.
|
|
288
|
+
blameToggle: 'Blame',
|
|
289
|
+
blameHint: '显示每一行最后是被谁、在哪个提交里改的',
|
|
290
|
+
blameUncommitted: '尚未提交',
|
|
291
|
+
blameFailed: '这个文件没有可追溯的历史(可能是未跟踪的新文件)。',
|
|
292
|
+
blameTruncated: '文件过长,追溯信息只显示了前面一部分。',
|
|
293
|
+
crlfNotice: '这个文件的行尾是 CRLF,暂不支持在线编辑(编辑框会把行尾统一成 LF,保存时整份文件都会被改写);查看和按块暂存/撤回不受影响。',
|
|
294
|
+
encodingNotice: '这个文件不是 UTF-8 编码(可能是 GBK、Shift JIS 之类),暂不支持在线编辑:页面上看到的文字是一次有损解码,保存回去会把文件里每一个非 ASCII 字节都改写掉,包括你没动过的行。查看和按块暂存/撤回不受影响。',
|
|
295
|
+
saveFailed: '保存失败',
|
|
296
|
+
saveUnavailable: '当前宿主还不支持保存(需要重启 dsh web 加载新版宿主端)。',
|
|
297
|
+
saveRetry: '重试保存',
|
|
298
|
+
staleTitle: '这个文件在你编辑期间被修改了',
|
|
299
|
+
staleBody: '磁盘上的内容已不是编辑器打开时的那份(保存时会被拒绝,文件保持原样)。可以放弃你的修改重新加载,也可以用编辑器里的内容覆盖磁盘上的新版本。',
|
|
300
|
+
staleReload: '重新加载(丢弃修改)',
|
|
301
|
+
staleOverwrite: '用编辑器内容覆盖',
|
|
302
|
+
unsavedTitle: '有未保存的修改',
|
|
303
|
+
unsavedBody: '{path} 有未保存的修改,切换后将丢弃这些修改。',
|
|
304
|
+
unsavedLeave: '仍要切换',
|
|
305
|
+
unsavedStay: '继续编辑',
|
|
208
306
|
'op.fail.auth': '认证失败。凭据提示已被禁用,请先在终端里配置好凭据再重试。',
|
|
209
307
|
'op.fail.network': '网络不可达:主机名解析失败或连接不上。检查网络与远程地址后重试。',
|
|
210
308
|
'op.fail.no-upstream': '当前分支没有上游分支。',
|
|
@@ -212,6 +310,8 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
212
310
|
'op.fail.conflict': '出现冲突,工作区已被改动。请在编辑器里解决冲突后继续。',
|
|
213
311
|
'op.fail.nothing-to-commit': '暂存区是空的,没有可提交的内容。',
|
|
214
312
|
'op.fail.dirty': '本地改动会被覆盖。先提交或暂存它们。',
|
|
313
|
+
'op.fail.stale': 'diff 已过期。',
|
|
314
|
+
'op.fail.invalid': '无效的操作。',
|
|
215
315
|
'op.fail.unknown': '操作失败。',
|
|
216
316
|
}
|
|
217
317
|
|
|
@@ -227,6 +327,23 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
227
327
|
tabChanges: 'Changes',
|
|
228
328
|
tabHistory: 'History',
|
|
229
329
|
tabCompare: 'Compare',
|
|
330
|
+
tabFiles: 'Files',
|
|
331
|
+
fileSearchPlaceholder: 'Search files…',
|
|
332
|
+
filesTruncated: 'Too many files to list; use the search for the rest.',
|
|
333
|
+
filesEmpty: 'This repository has no files yet',
|
|
334
|
+
filesNoMatch: 'No matching files',
|
|
335
|
+
filesMore: 'and {count} more — use the search above',
|
|
336
|
+
filesPick: 'Pick a file on the left to open it',
|
|
337
|
+
filesVanished: '{path} is not in the repository any more — deleted, renamed, or on a branch that no longer has it.',
|
|
338
|
+
filesUnsavedAsk: 'You have unsaved edits; opening another file discards them.',
|
|
339
|
+
filesDiscardOpen: 'Discard and open',
|
|
340
|
+
fileReadOnlyCrlf: 'This file uses CRLF line endings, so it opens read-only (the editor normalises them to LF, and a save would rewrite every line).',
|
|
341
|
+
fileReadOnlyEncoding: 'This file is not UTF-8, so it opens read-only: the text shown is a lossy decode, and saving it back would rewrite every non-ASCII byte.',
|
|
342
|
+
blameLine: 'Line {line}',
|
|
343
|
+
paintTooLarge: 'This file is long, so syntax colouring is off (a full repaint costs over a second, and a freeze on every pause is worse than plain text). Editing, saving and blame are unaffected.',
|
|
344
|
+
blameInHistory: 'Their commits on this file',
|
|
345
|
+
blamePick: 'Click a name to see which commit changed that line',
|
|
346
|
+
blameWhileEditing: 'Blame is hidden while you type: edited line numbers no longer match the commits behind them. It returns once you save or revert.',
|
|
230
347
|
historyEnd: 'Start of history',
|
|
231
348
|
loading: 'Loading…',
|
|
232
349
|
maximize: 'Maximize',
|
|
@@ -272,14 +389,32 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
272
389
|
styleFailed: 'Could not save',
|
|
273
390
|
resizeLabel: 'Drag to resize the drawer',
|
|
274
391
|
resizeCommits: 'Drag to resize the commit list',
|
|
392
|
+
historyLayout: 'Commit list layout',
|
|
393
|
+
layoutColumns: 'Side by side: list beside the diff',
|
|
394
|
+
layoutStacked: 'Stacked: list across the top',
|
|
275
395
|
resizeTree: 'Drag to resize the file tree',
|
|
396
|
+
resizeSides: 'Drag to resize the two columns',
|
|
276
397
|
sourceLabel: 'Switch stats source',
|
|
277
398
|
workingTree: 'Working tree',
|
|
278
399
|
loadingCommit: 'Loading commit…',
|
|
279
400
|
renamedFrom: 'Renamed from',
|
|
280
401
|
binaryFile: 'Binary file — no text diff',
|
|
402
|
+
imageBroken: 'The browser could not decode this image (the header checks out, so the file is probably damaged or truncated).',
|
|
403
|
+
imageFit: 'Fit',
|
|
404
|
+
imageActual: 'Actual size',
|
|
405
|
+
imageTooLarge: 'This image is {size}, past the {cap} preview limit.',
|
|
406
|
+
imageSource: 'Source',
|
|
407
|
+
imagePreview: 'Preview',
|
|
408
|
+
prevChange: 'Previous change',
|
|
409
|
+
nextChange: 'Next change',
|
|
410
|
+
prevChangeHint: 'Previous change (Shift+F7)',
|
|
411
|
+
nextChangeHint: 'Next change (F7)',
|
|
412
|
+
changeCount: 'changes: {n}',
|
|
281
413
|
loadingDiff: 'Loading diff…',
|
|
282
414
|
noTextDiff: 'No text changes',
|
|
415
|
+
tabUnstaged: 'Unstaged',
|
|
416
|
+
tabStaged: 'Staged',
|
|
417
|
+
diffTooLarge: 'File too large (over 20,000 lines or 2 MB) — showing the plain diff view',
|
|
283
418
|
noCommits: 'No commit history',
|
|
284
419
|
historyLabel: 'Commit history',
|
|
285
420
|
commitAuthor: 'Author',
|
|
@@ -346,6 +481,7 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
346
481
|
'op.ok.pull': 'Pulled',
|
|
347
482
|
'op.ok.push': 'Pushed',
|
|
348
483
|
'op.ok.discardFile': 'Rolled back',
|
|
484
|
+
'op.ok.applyBlocks': 'Applied',
|
|
349
485
|
discardAction: 'Roll back changes',
|
|
350
486
|
discardTitle: 'Roll back changes?',
|
|
351
487
|
discardConfirm: 'Roll back',
|
|
@@ -353,6 +489,43 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
353
489
|
discardBodyRestore: '{path} goes back to its committed content. The {added} added and {deleted} deleted lines here cannot be recovered.',
|
|
354
490
|
discardBodyDelete: '{path} was never recorded by git. Deleting it cannot be undone.',
|
|
355
491
|
discardBodyUnrename: 'Undo the rename: {path} goes back to {previousPath}, and content changed along the way is lost.',
|
|
492
|
+
// One BLOCK, not the whole file: the side pane's roll-back states exactly
|
|
493
|
+
// which lines leave and that the working-tree file is rewritten to do it.
|
|
494
|
+
blockStage: 'Stage block',
|
|
495
|
+
blockDiscard: 'Roll back block',
|
|
496
|
+
blockUnstage: 'Unstage block',
|
|
497
|
+
blockDiscardBody: 'This block of {path} ({added} added, {deleted} deleted lines) is rolled back and the working-tree file rewritten to do it. This cannot be undone.',
|
|
498
|
+
// The untracked case: the file's whole content is the one block, so rolling
|
|
499
|
+
// the block back reverse-applies the new-file patch and DELETES the file —
|
|
500
|
+
// the same consequence class the file-level delete wording exists to name.
|
|
501
|
+
blockDiscardBodyDelete: '{path} was never recorded by git, and this block is its whole content — rolling it back deletes the file. This cannot be undone.',
|
|
502
|
+
// The editable right column: editing arms explicitly (never per keystroke —
|
|
503
|
+
// an autosave racing a concurrent agent write is the data-loss case), and
|
|
504
|
+
// every save carries the sha the editor opened with.
|
|
505
|
+
editFile: 'Edit',
|
|
506
|
+
fileSave: 'Save (Ctrl/Cmd+S)',
|
|
507
|
+
fileRevert: 'Discard edits',
|
|
508
|
+
editingNotice: 'Editing with unsaved changes: block actions wait until the file is saved, and leaving first — another tab, another file, closing the drawer — asks.',
|
|
509
|
+
// Why the edit affordance is withheld on a CRLF file: the editor control
|
|
510
|
+
// itself rewrites \r\n to \n, so any save would change every line ending.
|
|
511
|
+
blameToggle: 'Blame',
|
|
512
|
+
blameHint: 'Show which commit last changed each line',
|
|
513
|
+
blameUncommitted: 'Not committed yet',
|
|
514
|
+
blameFailed: 'This file has no history to blame (it may be a new, untracked file).',
|
|
515
|
+
blameTruncated: 'The file is long, so blame is shown for the first part only.',
|
|
516
|
+
crlfNotice: 'This file has CRLF line endings, which the editor does not support yet (the edit box would turn every ending into LF, so a save rewrites the whole file); viewing and block staging/rolling back still work.',
|
|
517
|
+
encodingNotice: 'This file is not UTF-8 (GBK, Shift JIS or similar), so the editor is unavailable: the text shown is a lossy decode of it, and saving that back would rewrite every non-ASCII byte in the file, including lines you never touched. Viewing and block staging/rolling back still work.',
|
|
518
|
+
saveFailed: 'Save failed',
|
|
519
|
+
saveUnavailable: 'This host does not support saving yet — restart dsh web to load the new host half.',
|
|
520
|
+
saveRetry: 'Retry save',
|
|
521
|
+
staleTitle: 'This file changed while you were editing it',
|
|
522
|
+
staleBody: 'What is on disk is no longer what the editor opened (a save is refused and nothing written). Reload and lose your edits, or overwrite the newer file with what the editor holds.',
|
|
523
|
+
staleReload: 'Reload (discard my edits)',
|
|
524
|
+
staleOverwrite: 'Overwrite with my version',
|
|
525
|
+
unsavedTitle: 'Unsaved edits',
|
|
526
|
+
unsavedBody: '{path} has unsaved edits; switching now discards them.',
|
|
527
|
+
unsavedLeave: 'Switch anyway',
|
|
528
|
+
unsavedStay: 'Keep editing',
|
|
356
529
|
'op.fail.auth': 'Authentication failed. Credential prompts are disabled here — set your credentials up in a terminal first.',
|
|
357
530
|
'op.fail.network': 'The network was unreachable — the host could not be resolved or the connection failed. Check connectivity and the remote URL, then retry.',
|
|
358
531
|
'op.fail.no-upstream': 'This branch has no upstream.',
|
|
@@ -360,5 +533,7 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
360
533
|
'op.fail.conflict': 'Conflicts. The working tree has been changed; resolve them in the editor before continuing.',
|
|
361
534
|
'op.fail.nothing-to-commit': 'Nothing staged to commit.',
|
|
362
535
|
'op.fail.dirty': 'Local changes would be overwritten. Commit or stash them first.',
|
|
536
|
+
'op.fail.stale': 'The diff is stale.',
|
|
537
|
+
'op.fail.invalid': 'Invalid operation.',
|
|
363
538
|
'op.fail.unknown': 'The operation failed.',
|
|
364
539
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How wide a dragged pane is allowed to be.
|
|
3
|
+
*
|
|
4
|
+
* A pane may grow only into space the others do not need, so the ceiling is
|
|
5
|
+
* the drawer minus its neighbour minus the floor under the diff. The part that
|
|
6
|
+
* broke is the word NEIGHBOUR: the History tab stacks the commit list across
|
|
7
|
+
* the top, where it is a sibling of the tree-and-diff row rather than a column
|
|
8
|
+
* beside the tree — and counting a full-drawer-wide list as the tree's
|
|
9
|
+
* neighbour makes the ceiling smaller than the floor. The tree then sits at its
|
|
10
|
+
* minimum whatever the pointer does, which reads as a divider that will not
|
|
11
|
+
* move.
|
|
12
|
+
*
|
|
13
|
+
* So the question is asked of the geometry instead of the tab: two boxes are
|
|
14
|
+
* neighbours when one actually ends where the other begins. That stays true if
|
|
15
|
+
* another tab is stacked later, and it cannot be got wrong by a rename.
|
|
16
|
+
*
|
|
17
|
+
* Pure: no React, no DOM — the caller measures, this decides.
|
|
18
|
+
*
|
|
19
|
+
* @module @young1lin/dsh-ui-gitworkbench/client/pane-size
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** As much of a `DOMRect` as any of this needs. */
|
|
23
|
+
export interface PaneBox {
|
|
24
|
+
readonly left: number
|
|
25
|
+
readonly right: number
|
|
26
|
+
readonly width: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Sub-pixel tolerance: two boxes a third of a pixel apart are touching. */
|
|
30
|
+
const EPSILON_PX = 1
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The width `other` contributes to a drag on `pane` — zero unless the two are
|
|
34
|
+
* side by side.
|
|
35
|
+
*
|
|
36
|
+
* @param other - the pane that might be in the way, or null when it is not on
|
|
37
|
+
* screen at all.
|
|
38
|
+
* @param pane - the pane being dragged.
|
|
39
|
+
*/
|
|
40
|
+
export function neighbourWidth(other: PaneBox | null, pane: PaneBox | null): number {
|
|
41
|
+
if (other === null || pane === null) return 0
|
|
42
|
+
// Either order: whichever is on the left, they are neighbours only if one's
|
|
43
|
+
// trailing edge meets the other's leading edge. A box stacked above spans
|
|
44
|
+
// the same columns as the one below, so neither test holds.
|
|
45
|
+
const beside = other.right <= pane.left + EPSILON_PX || pane.right <= other.left + EPSILON_PX
|
|
46
|
+
return beside ? other.width : 0
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Clamp a dragged width between the pane's floor and the room actually left.
|
|
51
|
+
*
|
|
52
|
+
* The outer `Math.max` keeps the range from inverting in a drawer too narrow to
|
|
53
|
+
* hold both floors — the pane then simply takes its minimum, rather than the
|
|
54
|
+
* clamp returning a number below it.
|
|
55
|
+
*
|
|
56
|
+
* @param next - the width the pointer implies.
|
|
57
|
+
* @param min - this pane's floor.
|
|
58
|
+
* @param drawer - the drawer's inner width.
|
|
59
|
+
* @param other - the neighbour's width, from {@link neighbourWidth}.
|
|
60
|
+
* @param minDiff - the floor under the diff, which is never dragged directly.
|
|
61
|
+
*/
|
|
62
|
+
export function clampPane(
|
|
63
|
+
next: number,
|
|
64
|
+
min: number,
|
|
65
|
+
drawer: number,
|
|
66
|
+
other: number,
|
|
67
|
+
minDiff: number,
|
|
68
|
+
): number {
|
|
69
|
+
const max = Math.max(min, drawer - other - minDiff)
|
|
70
|
+
return Math.min(Math.max(next, min), max)
|
|
71
|
+
}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The editable right column's decision core: when a fetched layer payload may
|
|
3
|
+
* replace the editor buffer and when it must not, what a save's answer does to
|
|
4
|
+
* the buffer's basis, what "dirty" means, and what any gesture that would drop
|
|
5
|
+
* the buffer must ask first.
|
|
6
|
+
*
|
|
7
|
+
* The host's sha check is the hard wall between the editor and a concurrent
|
|
8
|
+
* agent write; these rules are everything the UI does around that wall. The
|
|
9
|
+
* one with teeth is {@link applySides}: a refresh that lands over a DIRTY
|
|
10
|
+
* buffer keeps the buffer and only notes whether the file moved underneath —
|
|
11
|
+
* the tree refreshes, the editor does not, which is what makes polling safe to
|
|
12
|
+
* leave running while somebody is typing.
|
|
13
|
+
*
|
|
14
|
+
* Pure: no React, no CSS, no git. `tests/side-edit.test.ts` loads it directly.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `gitWorkbench/writeChecked`'s answer, mirrored from the host's `WriteResult`
|
|
19
|
+
* (the client re-declares host shapes rather than importing the host module,
|
|
20
|
+
* which pulls node and the RPC decorators).
|
|
21
|
+
*/
|
|
22
|
+
export interface WriteResult {
|
|
23
|
+
readonly ok: boolean
|
|
24
|
+
/** Present only on failure; `stale` is the sha refusal. */
|
|
25
|
+
readonly failure?: string
|
|
26
|
+
/** A sentence for the user; names the file on the stale path. */
|
|
27
|
+
readonly error?: string
|
|
28
|
+
/** The file's blob sha after a successful write, for the next save. */
|
|
29
|
+
readonly sha?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** What a fetched layer payload contributes to the editor's basis. */
|
|
33
|
+
export interface EditSides {
|
|
34
|
+
readonly targetText: string
|
|
35
|
+
readonly targetSha: string
|
|
36
|
+
/** Host's word that the working-tree file is not valid UTF-8. Optional so a
|
|
37
|
+
* payload from a host half older than this client reads as "fine" rather
|
|
38
|
+
* than as a refusal it cannot explain. */
|
|
39
|
+
readonly lossyEncoding?: boolean
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The editable right column's whole state, minus transient banners. */
|
|
43
|
+
export interface EditState {
|
|
44
|
+
/** Whether the right column is the editor at all (unstaged tab only). */
|
|
45
|
+
readonly armed: boolean
|
|
46
|
+
/** The editor's text — the only thing the user changes directly. */
|
|
47
|
+
readonly buffer: string
|
|
48
|
+
/** Blob sha the buffer is based on: `writeChecked`'s `expectedSha`. */
|
|
49
|
+
readonly baseSha: string
|
|
50
|
+
/** The text `baseSha` names; dirty is buffer against THIS, not the disk. */
|
|
51
|
+
readonly baseText: string
|
|
52
|
+
/** The file changed underneath (fresh targetSha !== baseSha) while dirty. */
|
|
53
|
+
readonly conflict: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Before the first arm: nothing editable, no basis, nothing conflicting. */
|
|
57
|
+
export const DISARMED: EditState = { armed: false, buffer: '', baseSha: '', baseText: '', conflict: false }
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Whether the buffer holds edits no save has landed yet.
|
|
61
|
+
*
|
|
62
|
+
* Dirty is measured against `baseText` — the text of the last save or load —
|
|
63
|
+
* not against what a poll says the disk holds: the poll's view is a snapshot
|
|
64
|
+
* old by up to one interval, and "the agent changed the file" is a different
|
|
65
|
+
* question from "you have unsaved edits".
|
|
66
|
+
*/
|
|
67
|
+
export function isDirty(edit: EditState): boolean {
|
|
68
|
+
return edit.armed && edit.buffer !== edit.baseText
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Whether the editor may hold this payload's text: it must carry no carriage
|
|
73
|
+
* return.
|
|
74
|
+
*
|
|
75
|
+
* The HTML textarea value API normalises `\r\n` to `\n` the moment text
|
|
76
|
+
* enters it, so an armed CRLF file (an `core.autocrlf=true` checkout, or
|
|
77
|
+
* `eol=crlf`) would save ANY later edit with every line ending rewritten —
|
|
78
|
+
* a whole-file diff the reader never asked for. The first cut declines the
|
|
79
|
+
* edit rather than translate endings on the way out: the file stays viewable
|
|
80
|
+
* and the block actions keep working (they act on git's diff, not on the
|
|
81
|
+
* buffer); only the editor is withheld, with a notice saying why.
|
|
82
|
+
*/
|
|
83
|
+
export type ArmRefusal = 'encoding' | 'crlf'
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Why the editor is withheld for this payload, or null when it may arm.
|
|
87
|
+
*
|
|
88
|
+
* Two reasons, and the pane says WHICH: a reader shown one sentence for two
|
|
89
|
+
* different causes cannot tell whether converting line endings would help.
|
|
90
|
+
*
|
|
91
|
+
* - `encoding` — the file is not UTF-8 (GBK, Shift JIS, Latin-1). The text
|
|
92
|
+
* on screen is a lossy decode of it, so saving it back would replace every
|
|
93
|
+
* non-ASCII byte in the file, including lines nobody touched. The host
|
|
94
|
+
* refuses such a save too; this is what keeps the reader from reaching it.
|
|
95
|
+
* - `crlf` — the text carries carriage returns. The textarea normalises
|
|
96
|
+
* `{CR}{LF}` to `{LF}` the moment the text lands in it, so the next save
|
|
97
|
+
* would rewrite every line ending in the file.
|
|
98
|
+
*
|
|
99
|
+
* Encoding is reported first: it is the more fundamental of the two, and its
|
|
100
|
+
* sentence is the one that tells the reader nothing here will help.
|
|
101
|
+
*
|
|
102
|
+
* Either way only the EDITOR is withheld — the diff still renders and the
|
|
103
|
+
* block actions still work, because those act on git's own diff rather than
|
|
104
|
+
* on the buffer.
|
|
105
|
+
*/
|
|
106
|
+
export function armRefusal(sides: EditSides): ArmRefusal | null {
|
|
107
|
+
if (sides.lossyEncoding === true) return 'encoding'
|
|
108
|
+
if (sides.targetText.includes('\r')) return 'crlf'
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function editableSides(sides: EditSides): boolean {
|
|
113
|
+
return armRefusal(sides) === null
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The adopt core: buffer and basis become the payload, conflict cleared. */
|
|
117
|
+
function adopt(armed: boolean, sides: EditSides): EditState {
|
|
118
|
+
return { armed, buffer: sides.targetText, baseSha: sides.targetSha, baseText: sides.targetText, conflict: false }
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Arm the editor from the payload currently on screen: the buffer starts as
|
|
123
|
+
* the working-tree text, and the first save is checked against its sha.
|
|
124
|
+
* A payload {@link editableSides} refuses is handed back unchanged — the
|
|
125
|
+
* caller shows the notice instead of an editor.
|
|
126
|
+
*/
|
|
127
|
+
export function armEdit(edit: EditState, sides: EditSides): EditState {
|
|
128
|
+
return editableSides(sides) ? adopt(true, sides) : edit
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* A fetched payload lands over the editor.
|
|
133
|
+
*
|
|
134
|
+
* Clean (or disarmed): adopt — the editor follows the file, so the drawer
|
|
135
|
+
* keeps up with the agent exactly as it did before editing existed, and an
|
|
136
|
+
* armed editor whose file turned CRLF underneath disarms with the payload
|
|
137
|
+
* adopted (clean costs no edits, and an armed editor must never hold text
|
|
138
|
+
* the textarea would normalise). Dirty: keep the buffer whatever the payload
|
|
139
|
+
* says (a poll must never overwrite unsaved edits) and record whether the
|
|
140
|
+
* file itself moved, which is what the reload-and-lose-edits banner keys on.
|
|
141
|
+
* An index-only change (same targetSha) leaves the flag down: the diff moved,
|
|
142
|
+
* the edited file did not.
|
|
143
|
+
*/
|
|
144
|
+
export function applySides(edit: EditState, sides: EditSides): EditState {
|
|
145
|
+
if (!isDirty(edit)) {
|
|
146
|
+
return adopt(edit.armed && editableSides(sides), sides)
|
|
147
|
+
}
|
|
148
|
+
return { ...edit, conflict: sides.targetSha !== edit.baseSha }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The banner's reload action, on the user's explicit say-so: drop the edits
|
|
153
|
+
* onto the fresh text with a clean, current basis — and disarmed when the
|
|
154
|
+
* fresh text carries CRLF, for the same reason arming refuses it.
|
|
155
|
+
*/
|
|
156
|
+
export function reloadSides(edit: EditState, sides: EditSides): EditState {
|
|
157
|
+
return adopt(editableSides(sides), sides)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Another file or layer selected: the edit session belonged to what was on
|
|
162
|
+
* screen, so the editor disarms and adopts the new payload from scratch.
|
|
163
|
+
*/
|
|
164
|
+
export function resetSides(edit: EditState, sides: EditSides): EditState {
|
|
165
|
+
return adopt(false, sides)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** The drawer-level unsaved-edits guard's state. */
|
|
169
|
+
export interface LeaveGuard {
|
|
170
|
+
/** The pane's last dirty report — the pane is this flag's only writer. */
|
|
171
|
+
readonly dirty: boolean
|
|
172
|
+
/** Whether the unsaved-edits dialog is holding a deferred gesture. */
|
|
173
|
+
readonly askOpen: boolean
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** No edits, no open ask: the guard at rest. */
|
|
177
|
+
export const LEAVE_GUARD_CLEAR: LeaveGuard = { dirty: false, askOpen: false }
|
|
178
|
+
|
|
179
|
+
/** What a gesture that would drop the editor's surface should do. */
|
|
180
|
+
export type LeaveGate = { readonly kind: 'run' } | { readonly kind: 'ask' } | { readonly kind: 'wait' }
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Decide a leave gesture — a layer tab, a file selection, the drawer's close,
|
|
184
|
+
* a main-tab switch — against unsaved edits.
|
|
185
|
+
*
|
|
186
|
+
* `same` names a gesture whose target is what the pane already shows: the
|
|
187
|
+
* already-active tab, the tree row of the file already rendered. Such a
|
|
188
|
+
* gesture changes nothing the pane renders and discards nothing, so it runs
|
|
189
|
+
* without asking — a dialog over it would promise "switching discards your
|
|
190
|
+
* edits" and then discard nothing, and a Leave answer on it used to disarm
|
|
191
|
+
* the guard entirely. For a real target: `run` acts now, `ask` opens the
|
|
192
|
+
* unsaved-edits dialog (Stay keeps editing, Leave drops), `wait` does
|
|
193
|
+
* nothing because an ask is already open — a second gesture waits on the
|
|
194
|
+
* first's answer rather than stacking a dialog on it.
|
|
195
|
+
*/
|
|
196
|
+
export function gateLeave(guard: LeaveGuard, same: boolean): LeaveGate {
|
|
197
|
+
if (same) return { kind: 'run' }
|
|
198
|
+
if (!guard.dirty) return { kind: 'run' }
|
|
199
|
+
return guard.askOpen ? { kind: 'wait' } : { kind: 'ask' }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** An ask opened: the gesture is held for the dialog's answer. */
|
|
203
|
+
export function leaveAsked(guard: LeaveGuard): LeaveGuard {
|
|
204
|
+
return { ...guard, askOpen: true }
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The dialog answered — Leave or Stay, both close the ask — and the dirty
|
|
209
|
+
* flag is deliberately NOT touched.
|
|
210
|
+
*
|
|
211
|
+
* The pane is the flag's only writer: a genuine navigation routes through
|
|
212
|
+
* `resetSides`, whose clean transition is what reports false, while a no-op
|
|
213
|
+
* gesture leaves the pane armed and dirty and the guard correctly still
|
|
214
|
+
* armed for the next one. Clearing the flag here — on the guess that "Leave
|
|
215
|
+
* means the buffer went away" — is exactly what let a Leave confirmed on a
|
|
216
|
+
* no-op gesture disarm the guard for every gesture after it.
|
|
217
|
+
*/
|
|
218
|
+
export function leaveAnswered(guard: LeaveGuard): LeaveGuard {
|
|
219
|
+
return { ...guard, askOpen: false }
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** The pane reports its dirty flag — the one transition that writes it. */
|
|
223
|
+
export function paneDirtyReport(guard: LeaveGuard, dirty: boolean): LeaveGuard {
|
|
224
|
+
return { ...guard, dirty }
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* A successful `writeChecked`: the basis becomes the saved text and the sha
|
|
229
|
+
* the host read back. The buffer is NOT touched — typing that landed while
|
|
230
|
+
* the write was in flight stays dirty against the new basis, so it stays
|
|
231
|
+
* visible as unsaved rather than being silently declared written.
|
|
232
|
+
*/
|
|
233
|
+
export function applySaveOk(edit: EditState, savedText: string, sha: string): EditState {
|
|
234
|
+
return { ...edit, baseSha: sha, baseText: savedText, conflict: false }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* A refused save (`failure: 'stale'`): the buffer stands, the banner goes up.
|
|
239
|
+
* The reload/overwrite the banner offers reads the fresh sha off the next
|
|
240
|
+
* fetch, not off this refusal.
|
|
241
|
+
*/
|
|
242
|
+
export function markConflict(edit: EditState): EditState {
|
|
243
|
+
return { ...edit, conflict: true }
|
|
244
|
+
}
|