@yuhufe/wtool-vdiff 0.0.1
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/README.md +165 -0
- package/dist/DiffViewer/DiffViewer.vue.d.ts +12 -0
- package/dist/DiffViewer/MonacoDiffViewer.vue.d.ts +24 -0
- package/dist/DiffViewer/TopBar.vue.d.ts +13 -0
- package/dist/DiffViewer/createDiffViewer.d.ts +10 -0
- package/dist/DiffViewer/index.d.ts +1 -0
- package/dist/DiffViewer/useDiffView.d.ts +27 -0
- package/dist/DiffViewer/utils/autoHeight.d.ts +9 -0
- package/dist/DiffViewer/utils/patch2Pair.d.ts +27 -0
- package/dist/index.d.ts +2 -0
- package/dist/types.d.ts +20 -0
- package/dist/wtool-vdiff.cjs.js +48 -0
- package/dist/wtool-vdiff.css +1 -0
- package/dist/wtool-vdiff.es.d.ts +2 -0
- package/dist/wtool-vdiff.es.js +13026 -0
- package/package.json +53 -0
- package/src/DiffViewer/DiffViewer.vue +143 -0
- package/src/DiffViewer/MonacoDiffViewer.vue +178 -0
- package/src/DiffViewer/TopBar.vue +121 -0
- package/src/DiffViewer/createDiffViewer.ts +49 -0
- package/src/DiffViewer/index.ts +8 -0
- package/src/DiffViewer/useDiffView.ts +16 -0
- package/src/DiffViewer/utils/autoHeight.ts +149 -0
- package/src/DiffViewer/utils/patch2Pair.ts +121 -0
- package/src/index.ts +2 -0
- package/src/types.ts +20 -0
- package/src/vite-env.d.ts +7 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { WtoolDiffViewerProps } from '@/types'
|
|
2
|
+
import { parseHunks } from './patch2Pair'
|
|
3
|
+
|
|
4
|
+
const LINE_HEIGHT = 18
|
|
5
|
+
|
|
6
|
+
interface CommonParams {
|
|
7
|
+
minLine: number
|
|
8
|
+
maxLine: number
|
|
9
|
+
unchangedVisiable: boolean
|
|
10
|
+
unchangedCtxLineNum: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 在线可见行计数器,按顺序逐个喂入变更区间(1-based inclusive),流式累计可见行数。
|
|
15
|
+
* 区间必须按 start 升序喂入,每次 feed 后可读取 visible 判断是否达到 maxLine。
|
|
16
|
+
* @param totalLines 文件总行数,用于 clamp context 窗口上界及判断尾部是否有折叠 widget
|
|
17
|
+
* @param ctx 每个变更块上下保留的 context 行数,对应 Monaco contextLineCount
|
|
18
|
+
* @param maxLine 可见行上限,达到后 maxReached 为 true,调用方应立即停止 feed
|
|
19
|
+
*/
|
|
20
|
+
function makeVisibleLineCounter(totalLines: number, ctx: number, maxLine: number) {
|
|
21
|
+
let visible = 0 // 已 commit 的确定可见行数(不含当前 pending 块)
|
|
22
|
+
let pendingStart = -1 // 当前待合并窗口的起始行(-1 表示无 pending)
|
|
23
|
+
let pendingEnd = -1 // 当前待合并窗口的结束行
|
|
24
|
+
|
|
25
|
+
const commitPending = () => {
|
|
26
|
+
if (pendingStart === -1) return
|
|
27
|
+
if (pendingStart > 1) visible += 1
|
|
28
|
+
visible += pendingEnd - pendingStart + 1
|
|
29
|
+
pendingStart = -1
|
|
30
|
+
pendingEnd = -1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
feed(s: number, e: number) {
|
|
35
|
+
const winStart = Math.max(1, s - ctx)
|
|
36
|
+
const winEnd = Math.min(totalLines, e + ctx)
|
|
37
|
+
if (pendingEnd === -1 || winStart > pendingEnd + 1) {
|
|
38
|
+
commitPending() // 新窗口与 pending 有间隙:先提交 pending,再开新窗口
|
|
39
|
+
pendingStart = winStart
|
|
40
|
+
pendingEnd = winEnd
|
|
41
|
+
} else {
|
|
42
|
+
pendingEnd = Math.max(pendingEnd, winEnd) // 新窗口与 pending 重叠:合并,延伸末端
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
flush() {
|
|
46
|
+
commitPending()
|
|
47
|
+
if (visible > 0 && pendingEnd !== totalLines) visible += 1
|
|
48
|
+
return Math.min(visible, maxLine)
|
|
49
|
+
},
|
|
50
|
+
get visible() {
|
|
51
|
+
return visible
|
|
52
|
+
},
|
|
53
|
+
get maxReached() {
|
|
54
|
+
return visible >= maxLine
|
|
55
|
+
},
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const autoHeightPatch = function ({
|
|
60
|
+
patch,
|
|
61
|
+
minLine,
|
|
62
|
+
maxLine,
|
|
63
|
+
unchangedCtxLineNum,
|
|
64
|
+
}: {
|
|
65
|
+
patch: string
|
|
66
|
+
} & CommonParams): number {
|
|
67
|
+
const { hunks } = parseHunks(patch)
|
|
68
|
+
if (hunks.length === 0) return minLine
|
|
69
|
+
|
|
70
|
+
const lastHunk = hunks[hunks.length - 1]
|
|
71
|
+
const totalLines = Math.max(
|
|
72
|
+
lastHunk.origStart + lastHunk.origCount - 1,
|
|
73
|
+
lastHunk.modStart + lastHunk.modCount - 1,
|
|
74
|
+
)
|
|
75
|
+
const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, maxLine)
|
|
76
|
+
|
|
77
|
+
for (const h of hunks) {
|
|
78
|
+
const hunkEnd = Math.max(h.origStart + h.origCount - 1, h.modStart + h.modCount - 1)
|
|
79
|
+
counter.feed(h.origStart, hunkEnd)
|
|
80
|
+
if (counter.maxReached) return maxLine
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return Math.max(minLine, counter.flush())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const autoHeightPair = function ({
|
|
87
|
+
pair,
|
|
88
|
+
minLine,
|
|
89
|
+
maxLine,
|
|
90
|
+
unchangedVisiable,
|
|
91
|
+
unchangedCtxLineNum,
|
|
92
|
+
}: {
|
|
93
|
+
pair: WtoolDiffViewerProps['diffPair']
|
|
94
|
+
} & CommonParams): number {
|
|
95
|
+
if (!pair || pair.length < 2) return minLine
|
|
96
|
+
|
|
97
|
+
const origLines = pair[0].content.split('\n')
|
|
98
|
+
const modLines = pair[1].content.split('\n')
|
|
99
|
+
const totalLines = Math.max(origLines.length, modLines.length)
|
|
100
|
+
|
|
101
|
+
if (unchangedVisiable) {
|
|
102
|
+
return Math.max(minLine, Math.min(totalLines, maxLine))
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, maxLine)
|
|
106
|
+
let i = 0
|
|
107
|
+
while (i < totalLines) {
|
|
108
|
+
if ((origLines[i] ?? '') !== (modLines[i] ?? '')) {
|
|
109
|
+
const start = i + 1
|
|
110
|
+
while (i < totalLines && (origLines[i] ?? '') !== (modLines[i] ?? '')) i++
|
|
111
|
+
counter.feed(start, i)
|
|
112
|
+
if (counter.maxReached) return maxLine
|
|
113
|
+
} else {
|
|
114
|
+
i++
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return Math.max(minLine, counter.flush())
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const height2Num = (heightStr: string): number => {
|
|
122
|
+
if (heightStr.endsWith('vh')) {
|
|
123
|
+
const vh = parseFloat(heightStr)
|
|
124
|
+
return Math.round((vh / 100) * document.documentElement.clientHeight)
|
|
125
|
+
}
|
|
126
|
+
return Math.round(parseFloat(heightStr))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const autoHeight = function ({
|
|
130
|
+
patch,
|
|
131
|
+
pair,
|
|
132
|
+
maxHeight,
|
|
133
|
+
minHeight,
|
|
134
|
+
unchangedVisiable,
|
|
135
|
+
unchangedCtxLineNum,
|
|
136
|
+
}: {
|
|
137
|
+
patch?: string
|
|
138
|
+
pair?: WtoolDiffViewerProps['diffPair']
|
|
139
|
+
maxHeight: string
|
|
140
|
+
minHeight: string
|
|
141
|
+
unchangedVisiable: boolean
|
|
142
|
+
unchangedCtxLineNum: number
|
|
143
|
+
}): number {
|
|
144
|
+
const [minLine, maxLine] = [minHeight, maxHeight].map(str => Math.floor(height2Num(str) / LINE_HEIGHT))
|
|
145
|
+
const commonParams: CommonParams = { minLine, maxLine, unchangedVisiable, unchangedCtxLineNum }
|
|
146
|
+
|
|
147
|
+
if (patch) return autoHeightPatch({ patch, ...commonParams }) * LINE_HEIGHT
|
|
148
|
+
return autoHeightPair({ pair, ...commonParams }) * LINE_HEIGHT
|
|
149
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export interface FilePair {
|
|
2
|
+
filename: string
|
|
3
|
+
content: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface Hunk {
|
|
7
|
+
origStart: number // original 起始行号(1-based)
|
|
8
|
+
origCount: number // original 行数
|
|
9
|
+
modStart: number // modified 起始行号(1-based)
|
|
10
|
+
modCount: number // modified 行数
|
|
11
|
+
lines: string[] // hunk 原始行(含前缀字符)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 解析 unified diff patch,提取文件名与所有 hunk
|
|
16
|
+
*/
|
|
17
|
+
export function parseHunks(patch: string): { origFilename: string; modFilename: string; hunks: Hunk[] } {
|
|
18
|
+
const lines = patch.split('\n')
|
|
19
|
+
|
|
20
|
+
let origFilename = ''
|
|
21
|
+
let modFilename = ''
|
|
22
|
+
const hunks: Hunk[] = []
|
|
23
|
+
let currentHunk: Hunk | null = null
|
|
24
|
+
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
if (line.startsWith('--- ')) {
|
|
27
|
+
origFilename = line.slice(4).trim()
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
if (line.startsWith('+++ ')) {
|
|
31
|
+
modFilename = line.slice(4).trim()
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
// @@ -origStart,origCount +modStart,modCount @@
|
|
35
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/)
|
|
36
|
+
if (hunkMatch) {
|
|
37
|
+
currentHunk = {
|
|
38
|
+
origStart: parseInt(hunkMatch[1], 10),
|
|
39
|
+
origCount: hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1,
|
|
40
|
+
modStart: parseInt(hunkMatch[3], 10),
|
|
41
|
+
modCount: hunkMatch[4] !== undefined ? parseInt(hunkMatch[4], 10) : 1,
|
|
42
|
+
lines: [],
|
|
43
|
+
}
|
|
44
|
+
hunks.push(currentHunk)
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
if (currentHunk) {
|
|
48
|
+
if (line.startsWith('\\')) continue // ""
|
|
49
|
+
currentHunk.lines.push(line)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { origFilename, modFilename, hunks }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 将 unified diff patch 转换为 [original, modified] 文件对。
|
|
58
|
+
*
|
|
59
|
+
* Monaco diff editor 需要两个完整文件内容,而 patch 只记录变更片段。
|
|
60
|
+
* 未改动的行(hunk 之外)用空行填充,使两侧行号与 hunk 声明的起始位置对齐,
|
|
61
|
+
* 从而让 Monaco 能在正确的行号位置渲染增删差异。
|
|
62
|
+
*/
|
|
63
|
+
export const patch2Pair = function (patch: string): FilePair[] {
|
|
64
|
+
if (!patch) {
|
|
65
|
+
return [
|
|
66
|
+
{ filename: '', content: '' },
|
|
67
|
+
{ filename: '', content: '' },
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const { origFilename, modFilename, hunks } = parseHunks(patch)
|
|
72
|
+
|
|
73
|
+
const origLines: string[] = []
|
|
74
|
+
const modLines: string[] = []
|
|
75
|
+
|
|
76
|
+
// 追踪两侧各自已写到的行号(1-based)
|
|
77
|
+
let origCursor = 1
|
|
78
|
+
let modCursor = 1
|
|
79
|
+
|
|
80
|
+
for (const hunk of hunks) {
|
|
81
|
+
// hunk 之前未覆盖的行用空行填充,使光标推进到 hunk 起始位置
|
|
82
|
+
while (origCursor < hunk.origStart) {
|
|
83
|
+
origLines.push('')
|
|
84
|
+
origCursor++
|
|
85
|
+
}
|
|
86
|
+
while (modCursor < hunk.modStart) {
|
|
87
|
+
modLines.push('')
|
|
88
|
+
modCursor++
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const line of hunk.lines) {
|
|
92
|
+
const prefix = line[0]
|
|
93
|
+
const content = line.slice(1)
|
|
94
|
+
|
|
95
|
+
if (prefix === ' ') {
|
|
96
|
+
// context 行:两侧都保留原文
|
|
97
|
+
origLines.push(content)
|
|
98
|
+
modLines.push(content)
|
|
99
|
+
origCursor++
|
|
100
|
+
modCursor++
|
|
101
|
+
} else if (prefix === '-') {
|
|
102
|
+
// 删除行:original 保留,modified 用空行占位
|
|
103
|
+
origLines.push(content)
|
|
104
|
+
modLines.push('')
|
|
105
|
+
origCursor++
|
|
106
|
+
modCursor++
|
|
107
|
+
} else if (prefix === '+') {
|
|
108
|
+
// 新增行:modified 保留,original 用空行占位
|
|
109
|
+
origLines.push('')
|
|
110
|
+
modLines.push(content)
|
|
111
|
+
origCursor++
|
|
112
|
+
modCursor++
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return [
|
|
118
|
+
{ filename: origFilename, content: origLines.join('\n') },
|
|
119
|
+
{ filename: modFilename, content: modLines.join('\n') },
|
|
120
|
+
]
|
|
121
|
+
}
|
package/src/index.ts
ADDED
package/src/types.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type * as Monaco from 'monaco-editor'
|
|
2
|
+
|
|
3
|
+
export type DiffEditorOptions = Monaco.editor.IStandaloneDiffEditorConstructionOptions
|
|
4
|
+
export type ModelOptions = Monaco.editor.ITextModelUpdateOptions
|
|
5
|
+
|
|
6
|
+
export interface WtoolDiffViewerProps {
|
|
7
|
+
diffPair?: { filename: string; content: string }[]
|
|
8
|
+
diffPatch?: string
|
|
9
|
+
language?: string
|
|
10
|
+
options?: DiffEditorOptions
|
|
11
|
+
modelOptions?: ModelOptions
|
|
12
|
+
viewerStyle?: WtoolDiffViewerStyle
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface WtoolDiffViewerStyle {
|
|
16
|
+
width?: string
|
|
17
|
+
height?: string
|
|
18
|
+
minHeight?: string
|
|
19
|
+
maxHeight?: string
|
|
20
|
+
}
|