@young1lin/dsh-ui-gitworkbench 0.1.13 → 0.1.14
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 +6 -0
- package/CHANGELOG_EN.md +6 -0
- package/README.md +26 -0
- package/lib/client.js +349 -311
- package/package.json +1 -1
- package/src/client/DiffViews.tsx +41 -12
- package/src/client/cr-mark.ts +39 -0
- package/src/client/locales.ts +4 -4
- package/src/client/styles/changes.css +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@young1lin/dsh-ui-gitworkbench",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "Out-of-tree dsh web UI plugin: a session-header git workbench chip opening a drawer with the file tree, per-file diff, history, compare, staging, commit, and sync (fetch/pull/push).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
package/src/client/DiffViews.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from 'react'
|
|
5
5
|
|
|
6
6
|
import { attachWordRanges, gutterSides, overlayRanges, parseRows, type Row, type RowWithRanges } from './diff-model.ts'
|
|
7
|
+
import { CR, CR_GLYPH, splitOnCr } from './cr-mark.ts'
|
|
7
8
|
import { parsePatch } from '../patch-model.ts'
|
|
8
9
|
import { alignRows, allBlockLines, allBlockTally, blockActionsDisabled, blockCount, blockEdge, blockIsWholeFile, blockLines, blockTally, currentActionBlock, needsFirstBlockClearance, sideBodyState, type SideCell, type SideRow } from './side-rows.ts'
|
|
9
10
|
import { anchorFor, blockNearestTo, blockTopsFromRows, blockTopsFromSideRows, countBlocks, scrollTopFor, stepBlockIndex, unifiedBlocks } from './diff-nav.ts'
|
|
@@ -170,14 +171,25 @@ function rowClass(kind: Row['kind']): string {
|
|
|
170
171
|
function renderCode(row: RowWithRanges, tokens: readonly HighlightRun[]): ReactNode {
|
|
171
172
|
if (row.kind === 'hunk') return row.text
|
|
172
173
|
const painted = overlayRanges(tokens.length > 0 ? tokens : [{ text: row.text }], row.ranges ?? [])
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
174
|
+
// The trailing-CR marker rides AFTER everything painted: word ranges are
|
|
175
|
+
// char offsets into the row text, so a mid-line glyph would shift them.
|
|
176
|
+
// Mid-line CRs (a CR-only file) stay invisible here; the side pane draws those.
|
|
177
|
+
const crTail = row.text.endsWith(CR)
|
|
178
|
+
? <span className={css.crMark} aria-hidden="true">{CR_GLYPH}</span>
|
|
179
|
+
: null
|
|
180
|
+
if (painted.length === 1 && painted[0]!.color === undefined && !painted[0]!.mark) {
|
|
181
|
+
return crTail === null ? row.text : <>{row.text}{crTail}</>
|
|
182
|
+
}
|
|
183
|
+
return (<>
|
|
184
|
+
{painted.map((tok, i) => (
|
|
185
|
+
<span
|
|
186
|
+
key={i}
|
|
187
|
+
className={tok.mark ? (row.kind === 'add' ? css.wordAdd : css.wordDel) : undefined}
|
|
188
|
+
style={tok.color === undefined && !tok.italic ? undefined : { color: tok.color, fontStyle: tok.italic ? 'italic' : undefined }}
|
|
189
|
+
>{tok.text}</span>
|
|
190
|
+
))}
|
|
191
|
+
{crTail}
|
|
192
|
+
</>)
|
|
181
193
|
}
|
|
182
194
|
|
|
183
195
|
/* ---------- side-by-side diff rendering (working tree only) ---------- */
|
|
@@ -1043,16 +1055,33 @@ function sideCodeClass(row: SideRow, side: 'left' | 'right'): string {
|
|
|
1043
1055
|
return `${side === 'left' ? css.sideCodeDel : css.sideCodeAdd} ${css.sideCellBlock}`
|
|
1044
1056
|
}
|
|
1045
1057
|
|
|
1046
|
-
/** One
|
|
1058
|
+
/** One text with every carriage return drawn as the CR glyph. No CR means
|
|
1059
|
+
* the text comes back untouched — the common line, on both sides, costs one
|
|
1060
|
+
* `includes`. The glyph spans are aria-hidden and unselectable, so copying a
|
|
1061
|
+
* line copies code, not markers. */
|
|
1062
|
+
function renderWithCrMarks(text: string): ReactNode {
|
|
1063
|
+
const parts = splitOnCr(text)
|
|
1064
|
+
if (parts.length === 1) return text
|
|
1065
|
+
const out: ReactNode[] = [parts[0]!]
|
|
1066
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
1067
|
+
out.push(<span key={`cr${i}`} className={css.crMark} aria-hidden="true">{CR_GLYPH}</span>)
|
|
1068
|
+
out.push(parts[i]!)
|
|
1069
|
+
}
|
|
1070
|
+
return out
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
/** One cell's Shiki runs, or its plain text when no tokens exist; either way
|
|
1074
|
+
* each carriage return in the cell is drawn, so a line whose only change is
|
|
1075
|
+
* its ending shows the difference instead of two identical-looking cells. */
|
|
1047
1076
|
function renderSideCode(cell: SideCell | null, tokens: readonly HighlightRun[] | undefined): ReactNode {
|
|
1048
1077
|
if (cell === null) return ''
|
|
1049
|
-
if (tokens === undefined || tokens.length === 0) return cell.text
|
|
1050
|
-
if (tokens.length === 1 && tokens[0]!.color === undefined && !tokens[0]!.italic) return cell.text
|
|
1078
|
+
if (tokens === undefined || tokens.length === 0) return renderWithCrMarks(cell.text)
|
|
1079
|
+
if (tokens.length === 1 && tokens[0]!.color === undefined && !tokens[0]!.italic) return renderWithCrMarks(cell.text)
|
|
1051
1080
|
return tokens.map((tok, i) => (
|
|
1052
1081
|
<span
|
|
1053
1082
|
key={i}
|
|
1054
1083
|
style={tok.color === undefined && !tok.italic ? undefined : { color: tok.color, fontStyle: tok.italic ? 'italic' : undefined }}
|
|
1055
|
-
>{tok.text}</span>
|
|
1084
|
+
>{renderWithCrMarks(tok.text)}</span>
|
|
1056
1085
|
))
|
|
1057
1086
|
}
|
|
1058
1087
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Making the carriage return visible in diff views.
|
|
3
|
+
*
|
|
4
|
+
* A diff line's text carries the CR byte through verbatim (`fileSides` serves
|
|
5
|
+
* git's own bytes), and a browser draws nothing for it: two cells whose only
|
|
6
|
+
* difference is a line ending render as byte-identical text, so a whole-file
|
|
7
|
+
* CRLF rewrite reads as a wall of changed lines with no visible change in
|
|
8
|
+
* any of them. The fix is not to strip the CR — the byte must survive into
|
|
9
|
+
* every patch the block actions emit — but to draw a glyph at each CR while
|
|
10
|
+
* RENDERING, the way `git diff` spells it as `^M`.
|
|
11
|
+
*
|
|
12
|
+
* The glyph is U+240D (SYMBOL FOR CARRIAGE RETURN), the standard picture of
|
|
13
|
+
* the control character, so a marked line reads as "ends in CR" rather than
|
|
14
|
+
* as a stray character in the code.
|
|
15
|
+
*
|
|
16
|
+
* Pure: no React, no CSS, no git. `tests/cr-mark.test.ts` loads it directly.
|
|
17
|
+
*
|
|
18
|
+
* @module @young1lin/dsh-ui-gitworkbench/cr-mark
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Built rather than escaped so this source file survives any tool that
|
|
22
|
+
* normalises text-mode line endings on its way to disk. */
|
|
23
|
+
export const CR = String.fromCharCode(13)
|
|
24
|
+
|
|
25
|
+
/** What a carriage return is drawn as. */
|
|
26
|
+
export const CR_GLYPH = '\u240d'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Split text at every carriage return, so a renderer can interleave one
|
|
30
|
+
* {@link CR_GLYPH} span between consecutive parts.
|
|
31
|
+
*
|
|
32
|
+
* @param text - one cell's text, as the diff model carries it.
|
|
33
|
+
* @returns the parts between CRs; a text with no CR comes back as itself in
|
|
34
|
+
* one piece (length 1), which is the caller's cheap early-out.
|
|
35
|
+
*/
|
|
36
|
+
export function splitOnCr(text: string): readonly string[] {
|
|
37
|
+
if (!text.includes(CR)) return [text]
|
|
38
|
+
return text.split(CR)
|
|
39
|
+
}
|
package/src/client/locales.ts
CHANGED
|
@@ -99,7 +99,7 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
99
99
|
filesDiscardOpen: '丢弃并打开',
|
|
100
100
|
// Read-only rather than withheld: in this view READING the file is the
|
|
101
101
|
// point, and only saving it back would rewrite bytes nobody touched.
|
|
102
|
-
fileReadOnlyCrlf: '这个文件的行尾是 CRLF,只能查看不能编辑(编辑器会把行尾统一成 LF
|
|
102
|
+
fileReadOnlyCrlf: '这个文件的行尾是 CRLF,只能查看不能编辑(编辑器会把行尾统一成 LF,保存时整份文件都会被改写);建议把行尾统一成 LF。',
|
|
103
103
|
fileReadOnlyEncoding: '这个文件不是 UTF-8 编码,只能查看不能编辑:页面上的文字是一次有损解码,保存回去会改写每一个非 ASCII 字节。',
|
|
104
104
|
blameLine: '第 {line} 行',
|
|
105
105
|
blameInHistory: '他在本文件的提交',
|
|
@@ -292,7 +292,7 @@ export const zh: Record<WorkbenchKey, string> = {
|
|
|
292
292
|
blameUncommitted: '尚未提交',
|
|
293
293
|
blameFailed: '这个文件没有可追溯的历史(可能是未跟踪的新文件)。',
|
|
294
294
|
blameTruncated: '文件过长,追溯信息只显示了前面一部分。',
|
|
295
|
-
crlfNotice: '这个文件的行尾是 CRLF,暂不支持在线编辑(编辑框会把行尾统一成 LF
|
|
295
|
+
crlfNotice: '这个文件的行尾是 CRLF,暂不支持在线编辑(编辑框会把行尾统一成 LF,保存时整份文件都会被改写);建议把行尾统一成 LF。差异里的回车已用 ␍ 标出;查看和按块暂存/撤回不受影响。',
|
|
296
296
|
encodingNotice: '这个文件不是 UTF-8 编码(可能是 GBK、Shift JIS 之类),暂不支持在线编辑:页面上看到的文字是一次有损解码,保存回去会把文件里每一个非 ASCII 字节都改写掉,包括你没动过的行。查看和按块暂存/撤回不受影响。',
|
|
297
297
|
saveFailed: '保存失败',
|
|
298
298
|
saveUnavailable: '当前宿主还不支持保存(需要重启 dsh web 加载新版宿主端)。',
|
|
@@ -339,7 +339,7 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
339
339
|
filesVanished: '{path} is not in the repository any more — deleted, renamed, or on a branch that no longer has it.',
|
|
340
340
|
filesUnsavedAsk: 'You have unsaved edits; opening another file discards them.',
|
|
341
341
|
filesDiscardOpen: 'Discard and open',
|
|
342
|
-
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).',
|
|
342
|
+
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); normalising the endings to LF is recommended.',
|
|
343
343
|
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.',
|
|
344
344
|
blameLine: 'Line {line}',
|
|
345
345
|
blameInHistory: 'Their commits on this file',
|
|
@@ -517,7 +517,7 @@ export const en: Record<WorkbenchKey, string> = {
|
|
|
517
517
|
blameUncommitted: 'Not committed yet',
|
|
518
518
|
blameFailed: 'This file has no history to blame (it may be a new, untracked file).',
|
|
519
519
|
blameTruncated: 'The file is long, so blame is shown for the first part only.',
|
|
520
|
-
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.',
|
|
520
|
+
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); normalising the endings to LF is recommended. Carriage returns are marked ␍ in the diff; viewing and block staging/rolling back still work.',
|
|
521
521
|
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.',
|
|
522
522
|
saveFailed: 'Save failed',
|
|
523
523
|
saveUnavailable: 'This host does not support saving yet — restart dsh web to load the new host half.',
|
|
@@ -279,6 +279,14 @@
|
|
|
279
279
|
|
|
280
280
|
.wordAdd { background: var(--gs-add-word); border-radius: 2px; }
|
|
281
281
|
.wordDel { background: var(--gs-del-word); border-radius: 2px; }
|
|
282
|
+
/* A carriage return, drawn. The diff's text carries the CR byte through
|
|
283
|
+
verbatim and a browser paints nothing for it, so a line whose only change
|
|
284
|
+
is its ending rendered as two identical-looking cells; the glyph (U+240D,
|
|
285
|
+
the picture of the control character — `cr-mark.ts`) makes the ending
|
|
286
|
+
visible wherever the diff paints one. Dim and slightly small so a whole
|
|
287
|
+
CRLF file reads as code with quiet markers, not as a wall of symbols, and
|
|
288
|
+
unselectable so copying a line copies code. */
|
|
289
|
+
.crMark { color: var(--gs-fg-faint); font-size: 0.9em; user-select: none; }
|
|
282
290
|
|
|
283
291
|
/* Side-by-side diff pane. One grid holds EVERY row's four cells — a per-row
|
|
284
292
|
grid would let the column boundary drift with each row's content, and a
|