@yuhufe/wtool-vdiff 0.0.5 → 0.0.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/dist/DiffViewer/DiffViewer.vue.d.ts +1 -1
- package/dist/DiffViewer/MonacoDiffViewer.vue.d.ts +1 -1
- package/dist/DiffViewer/const.d.ts +1 -0
- package/dist/DiffViewer/utils/autoHeight.d.ts +2 -1
- package/dist/DiffViewer/utils/patch2Pair.d.ts +0 -15
- package/dist/types.d.ts +6 -0
- package/dist/utils/patch.d.ts +20 -0
- package/dist/wtool-vdiff.cjs.js +39 -33
- package/dist/wtool-vdiff.es.js +3480 -3288
- package/package.json +5 -3
- package/src/DiffFiles/DiffFiles.vue +47 -0
- package/src/DiffFiles/FileExplore/FileExplore.vue +23 -0
- package/src/DiffViewer/DiffViewer.vue +10 -3
- package/src/DiffViewer/TopBar.vue +2 -2
- package/src/DiffViewer/const.ts +1 -0
- package/src/DiffViewer/utils/autoHeight.ts +117 -48
- package/src/DiffViewer/utils/patch2Pair.ts +4 -51
- package/src/types.ts +7 -0
- package/src/utils/patch.ts +74 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yuhufe/wtool-vdiff",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.7",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Monaco diff viewer as Vue 3 Web Component",
|
|
7
7
|
"keywords": [
|
|
@@ -35,13 +35,15 @@
|
|
|
35
35
|
"vue": "^3.5.0"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@yuhufe/web-ui": "workspace:*",
|
|
39
|
+
"@yuhufe/web-common": "workspace:*",
|
|
38
40
|
"@monaco-editor/loader": "^1.6.1",
|
|
39
|
-
"
|
|
41
|
+
"diff": "^9.0.0",
|
|
40
42
|
"monaco-editor": "^0.53.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@vitejs/plugin-vue": "^5.2.0",
|
|
44
|
-
"
|
|
46
|
+
"less": "4.6.4",
|
|
45
47
|
"typescript": "^5.9.3",
|
|
46
48
|
"vite": "^7.3.1",
|
|
47
49
|
"vite-plugin-css-injected-by-js": "5.0.1",
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="diff-files-wrap">
|
|
3
|
+
<Toolbar class="toolbar" />
|
|
4
|
+
<div class="content-wrap">
|
|
5
|
+
<FileExplore :diffFiles="diffFiles" />
|
|
6
|
+
<div class="filelist-viewer-wrap"></div>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</template>
|
|
10
|
+
|
|
11
|
+
<script setup lang="ts">
|
|
12
|
+
import { ref, computed, nextTick } from 'vue'
|
|
13
|
+
|
|
14
|
+
import { loadingDirective } from '@yuhufe/web-ui'
|
|
15
|
+
import type { DiffEditorOptions, WtoolDiffViewerProps, ModelOptions } from '../types'
|
|
16
|
+
|
|
17
|
+
import FileExplore from './FileExplore/FileExplore.vue'
|
|
18
|
+
|
|
19
|
+
const vLoading = loadingDirective
|
|
20
|
+
const loading = ref(true)
|
|
21
|
+
|
|
22
|
+
const props = withDefaults(
|
|
23
|
+
defineProps<{
|
|
24
|
+
diffFiles: WtoolDiffViewerProps[]
|
|
25
|
+
}>(),
|
|
26
|
+
{
|
|
27
|
+
diffFiles: () => [],
|
|
28
|
+
}
|
|
29
|
+
)
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<style scoped>
|
|
33
|
+
.diff-files-wrap {
|
|
34
|
+
width: 100%;
|
|
35
|
+
height: 100%;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
|
|
39
|
+
.toolbar {
|
|
40
|
+
flex-shrink: 0;
|
|
41
|
+
}
|
|
42
|
+
.content-wrap {
|
|
43
|
+
flex: 1;
|
|
44
|
+
overflow: hidden;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
</style>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="file-explore-wrap"></div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup lang="ts">
|
|
6
|
+
import { ref, computed, nextTick, shallowRef } from 'vue'
|
|
7
|
+
|
|
8
|
+
import { loadingDirective } from '@yuhufe/web-ui'
|
|
9
|
+
import type { FileTree, WtoolDiffViewerProps, ModelOptions } from '@/types'
|
|
10
|
+
|
|
11
|
+
const props = withDefaults(
|
|
12
|
+
defineProps<{
|
|
13
|
+
diffFiles: WtoolDiffViewerProps[]
|
|
14
|
+
}>(),
|
|
15
|
+
{
|
|
16
|
+
diffFiles: () => [],
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const filesData = shallowRef<FileTree[]>([])
|
|
21
|
+
</script>
|
|
22
|
+
|
|
23
|
+
<style scoped></style>
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<div class="content-wrap" v-show="!viewed" v-loading="loading">
|
|
5
5
|
<MonacoDiffViewer
|
|
6
6
|
class="monaco-container"
|
|
7
|
+
:style="{ opacity: loading ? 0 : 1 }"
|
|
7
8
|
:originalCode="originalCode"
|
|
8
9
|
:modifiedCode="modifiedCode"
|
|
9
10
|
:language="language"
|
|
@@ -16,21 +17,24 @@
|
|
|
16
17
|
</template>
|
|
17
18
|
|
|
18
19
|
<script setup lang="ts">
|
|
19
|
-
import { ref, computed } from 'vue'
|
|
20
|
+
import { ref, computed, nextTick } from 'vue'
|
|
20
21
|
|
|
21
22
|
import { loadingDirective } from '@yuhufe/web-ui'
|
|
23
|
+
import { v4 } from '@yuhufe/web-common'
|
|
22
24
|
import type { DiffEditorOptions, WtoolDiffViewerProps, ModelOptions } from '../types'
|
|
23
25
|
import MonacoDiffViewer from './MonacoDiffViewer.vue'
|
|
24
26
|
import TopBar from './TopBar.vue'
|
|
25
27
|
import { useDiffViewer } from './useDiffView'
|
|
26
28
|
import { patch2Pair } from './utils/patch2Pair'
|
|
27
29
|
import { autoHeight } from './utils/autoHeight'
|
|
30
|
+
import { HEIGHT_TOP_BAR } from './const'
|
|
28
31
|
|
|
29
32
|
const vLoading = loadingDirective
|
|
30
33
|
const loading = ref(true)
|
|
34
|
+
const autoHeightId = v4()
|
|
31
35
|
|
|
32
36
|
const _renderStart = performance.now()
|
|
33
|
-
const onMonacoRenderComplete = () => {
|
|
37
|
+
const onMonacoRenderComplete = async () => {
|
|
34
38
|
const cost = performance.now() - _renderStart
|
|
35
39
|
console.log(`[DiffViewer] 渲染耗时: ${cost.toFixed(2)} ms`)
|
|
36
40
|
loading.value = false
|
|
@@ -77,6 +81,7 @@ const mergedOptions = computed(() => {
|
|
|
77
81
|
hideUnchangedRegions = {
|
|
78
82
|
enabled: true,
|
|
79
83
|
contextLineCount: 3,
|
|
84
|
+
minimumLineCount: 1,
|
|
80
85
|
...hideUnchangedRegions,
|
|
81
86
|
}
|
|
82
87
|
}
|
|
@@ -104,6 +109,7 @@ const viewerHeight = computed(() => {
|
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
const height = autoHeight({
|
|
112
|
+
id: autoHeightId,
|
|
107
113
|
patch: props.diffPatch,
|
|
108
114
|
pair: props.diffPair,
|
|
109
115
|
...heightRange,
|
|
@@ -111,7 +117,8 @@ const viewerHeight = computed(() => {
|
|
|
111
117
|
unchangedCtxLineNum: mergedOptions.value.hideUnchangedRegions.contextLineCount!,
|
|
112
118
|
})
|
|
113
119
|
|
|
114
|
-
|
|
120
|
+
// 当前高度为代码高度,需要加上 topBar 高度才是完整高度
|
|
121
|
+
return `${height + HEIGHT_TOP_BAR}px`
|
|
115
122
|
})
|
|
116
123
|
const viewerStyle = computed(() => {
|
|
117
124
|
return {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="top-bar-wrap">
|
|
2
|
+
<div class="top-bar-wrap" :style="{ height: `${HEIGHT_TOP_BAR}px` }">
|
|
3
3
|
<div class="title-area">
|
|
4
4
|
<div class="filename">{{ filenameDisplay }}</div>
|
|
5
5
|
<span :class="['diff-type-tag', diffType]">{{ diffTypeLabel }}</span>
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<script setup lang="ts">
|
|
25
25
|
import { computed, ref, onMounted } from 'vue'
|
|
26
26
|
import { useDiffViewer } from './useDiffView'
|
|
27
|
+
import { HEIGHT_TOP_BAR } from './const'
|
|
27
28
|
|
|
28
29
|
const { funcs, registerFunc } = useDiffViewer()
|
|
29
30
|
|
|
@@ -100,7 +101,6 @@ registerFunc({
|
|
|
100
101
|
<style scoped>
|
|
101
102
|
.top-bar-wrap {
|
|
102
103
|
box-sizing: border-box;
|
|
103
|
-
height: 32px;
|
|
104
104
|
|
|
105
105
|
display: flex;
|
|
106
106
|
overflow: hidden;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const HEIGHT_TOP_BAR = 32
|
|
@@ -1,30 +1,84 @@
|
|
|
1
|
+
import { diffLines } from 'diff'
|
|
1
2
|
import type { WtoolDiffViewerProps } from '@/types'
|
|
2
|
-
import {
|
|
3
|
+
import { parsePatchHunks } from '@/utils/patch'
|
|
3
4
|
|
|
4
5
|
const LINE_HEIGHT = 18
|
|
6
|
+
const GAP_HEIGHT = 24
|
|
7
|
+
const heightMap = new Map<string, Partial<Record<'visible' | 'hidden', number>>>()
|
|
5
8
|
|
|
6
9
|
interface CommonParams {
|
|
7
|
-
|
|
8
|
-
|
|
10
|
+
minPx: number
|
|
11
|
+
maxPx: number
|
|
9
12
|
unchangedVisiable: boolean
|
|
10
13
|
unchangedCtxLineNum: number
|
|
11
14
|
}
|
|
12
15
|
|
|
16
|
+
interface ChangedLineBlock {
|
|
17
|
+
start: number
|
|
18
|
+
end: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ChangedLineInfo {
|
|
22
|
+
blocks: ChangedLineBlock[]
|
|
23
|
+
totalLines: number
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function getChangedLineInfo(origContent: string, modContent: string): ChangedLineInfo {
|
|
27
|
+
const changes = diffLines(origContent, modContent)
|
|
28
|
+
const blocks: ChangedLineBlock[] = []
|
|
29
|
+
let row = 1
|
|
30
|
+
let removed = 0
|
|
31
|
+
let added = 0
|
|
32
|
+
|
|
33
|
+
const flush = () => {
|
|
34
|
+
const len = Math.max(removed, added)
|
|
35
|
+
if (len > 0) {
|
|
36
|
+
blocks.push({ start: row, end: row + len - 1 })
|
|
37
|
+
row += len
|
|
38
|
+
removed = 0
|
|
39
|
+
added = 0
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for (const part of changes) {
|
|
44
|
+
const count = part.count ?? 0
|
|
45
|
+
|
|
46
|
+
if (part.removed) {
|
|
47
|
+
removed += count
|
|
48
|
+
} else if (part.added) {
|
|
49
|
+
added += count
|
|
50
|
+
} else {
|
|
51
|
+
flush()
|
|
52
|
+
row += count
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
flush()
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
blocks,
|
|
60
|
+
totalLines: Math.max(0, row - 1),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
13
64
|
/**
|
|
14
65
|
* 在线可见行计数器,按顺序逐个喂入变更区间(1-based inclusive),流式累计可见行数。
|
|
15
|
-
* 区间必须按 start 升序喂入,每次 feed 后可读取
|
|
66
|
+
* 区间必须按 start 升序喂入,每次 feed 后可读取 maxReached 判断是否达到像素上限。
|
|
16
67
|
* @param totalLines 文件总行数,用于 clamp context 窗口上界及判断尾部是否有折叠 widget
|
|
17
68
|
* @param ctx 每个变更块上下保留的 context 行数,对应 Monaco contextLineCount
|
|
18
|
-
* @param
|
|
69
|
+
* @param maxPx 像素上限,达到后 maxReached 为 true,调用方应立即停止 feed
|
|
19
70
|
*/
|
|
20
|
-
function makeVisibleLineCounter(totalLines: number, ctx: number,
|
|
21
|
-
let visible = 0
|
|
71
|
+
function makeVisibleLineCounter(totalLines: number, ctx: number, maxPx: number) {
|
|
72
|
+
let visible = 0 // 已 commit 的确定可见行数(不含当前 pending 块)
|
|
73
|
+
let gaps = 0 // gap widget 数量(每个折叠区域占 GAP_HEIGHT,非 LINE_HEIGHT)
|
|
22
74
|
let pendingStart = -1 // 当前待合并窗口的起始行(-1 表示无 pending)
|
|
23
|
-
let pendingEnd = -1
|
|
75
|
+
let pendingEnd = -1 // 当前待合并窗口的结束行
|
|
76
|
+
|
|
77
|
+
const usedPx = () => visible * LINE_HEIGHT + gaps * GAP_HEIGHT
|
|
24
78
|
|
|
25
79
|
const commitPending = () => {
|
|
26
80
|
if (pendingStart === -1) return
|
|
27
|
-
if (pendingStart > 1)
|
|
81
|
+
if (pendingStart > 1) gaps += 1
|
|
28
82
|
visible += pendingEnd - pendingStart + 1
|
|
29
83
|
pendingStart = -1
|
|
30
84
|
pendingEnd = -1
|
|
@@ -35,7 +89,7 @@ function makeVisibleLineCounter(totalLines: number, ctx: number, maxLine: number
|
|
|
35
89
|
const winStart = Math.max(1, s - ctx)
|
|
36
90
|
const winEnd = Math.min(totalLines, e + ctx)
|
|
37
91
|
if (pendingEnd === -1 || winStart > pendingEnd + 1) {
|
|
38
|
-
commitPending()
|
|
92
|
+
commitPending() // 新窗口与 pending 有间隙:先提交 pending,再开新窗口
|
|
39
93
|
pendingStart = winStart
|
|
40
94
|
pendingEnd = winEnd
|
|
41
95
|
} else {
|
|
@@ -43,79 +97,82 @@ function makeVisibleLineCounter(totalLines: number, ctx: number, maxLine: number
|
|
|
43
97
|
}
|
|
44
98
|
},
|
|
45
99
|
flush() {
|
|
100
|
+
const lastEnd = pendingEnd
|
|
46
101
|
commitPending()
|
|
47
|
-
if (visible > 0 &&
|
|
48
|
-
return
|
|
102
|
+
if (visible > 0 && lastEnd !== totalLines) gaps += 1
|
|
103
|
+
return usedPx()
|
|
104
|
+
},
|
|
105
|
+
get usedPx() {
|
|
106
|
+
return usedPx()
|
|
49
107
|
},
|
|
50
|
-
get
|
|
51
|
-
return
|
|
108
|
+
get gaps() {
|
|
109
|
+
return gaps
|
|
52
110
|
},
|
|
53
111
|
get maxReached() {
|
|
54
|
-
return
|
|
112
|
+
return usedPx() >= maxPx
|
|
55
113
|
},
|
|
56
114
|
}
|
|
57
115
|
}
|
|
58
116
|
|
|
59
117
|
const autoHeightPatch = function ({
|
|
60
118
|
patch,
|
|
61
|
-
|
|
62
|
-
|
|
119
|
+
minPx,
|
|
120
|
+
maxPx,
|
|
63
121
|
unchangedCtxLineNum,
|
|
64
122
|
}: {
|
|
65
123
|
patch: string
|
|
66
124
|
} & CommonParams): number {
|
|
67
|
-
const
|
|
68
|
-
if (hunks.length === 0) return
|
|
125
|
+
const hunks = parsePatchHunks(patch)
|
|
126
|
+
if (hunks.length === 0) return minPx
|
|
69
127
|
|
|
70
128
|
const lastHunk = hunks[hunks.length - 1]
|
|
71
|
-
const totalLines = Math.max(
|
|
72
|
-
|
|
73
|
-
lastHunk.modStart + lastHunk.modCount - 1,
|
|
74
|
-
)
|
|
75
|
-
const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, maxLine)
|
|
129
|
+
const totalLines = Math.max(lastHunk.origStart + lastHunk.origCount - 1, lastHunk.modStart + lastHunk.modCount - 1)
|
|
130
|
+
const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, maxPx)
|
|
76
131
|
|
|
77
132
|
for (const h of hunks) {
|
|
78
133
|
const hunkEnd = Math.max(h.origStart + h.origCount - 1, h.modStart + h.modCount - 1)
|
|
79
134
|
counter.feed(h.origStart, hunkEnd)
|
|
80
|
-
if (counter.maxReached) return
|
|
135
|
+
if (counter.maxReached) return maxPx
|
|
81
136
|
}
|
|
82
137
|
|
|
83
|
-
return Math.max(
|
|
138
|
+
return Math.max(minPx, counter.flush())
|
|
84
139
|
}
|
|
85
140
|
|
|
86
141
|
const autoHeightPair = function ({
|
|
87
142
|
pair,
|
|
88
|
-
|
|
89
|
-
|
|
143
|
+
minPx,
|
|
144
|
+
maxPx,
|
|
90
145
|
unchangedVisiable,
|
|
91
146
|
unchangedCtxLineNum,
|
|
92
147
|
}: {
|
|
93
148
|
pair: WtoolDiffViewerProps['diffPair']
|
|
94
149
|
} & CommonParams): number {
|
|
95
|
-
if (!pair || pair.length < 2) return
|
|
150
|
+
if (!pair || pair.length < 2) return minPx
|
|
96
151
|
|
|
97
|
-
const
|
|
98
|
-
const
|
|
152
|
+
const origContent = pair[0].content ?? ''
|
|
153
|
+
const modContent = pair[1].content ?? ''
|
|
154
|
+
const origLines = origContent.split('\n')
|
|
155
|
+
const modLines = modContent.split('\n')
|
|
99
156
|
const totalLines = Math.max(origLines.length, modLines.length)
|
|
100
157
|
|
|
101
158
|
if (unchangedVisiable) {
|
|
102
|
-
return Math.max(
|
|
159
|
+
return Math.max(minPx, Math.min(totalLines * LINE_HEIGHT, maxPx))
|
|
103
160
|
}
|
|
104
161
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
if (counter.maxReached) return maxLine
|
|
113
|
-
} else {
|
|
114
|
-
i++
|
|
115
|
-
}
|
|
162
|
+
const { blocks, totalLines: diffTotalLines } = getChangedLineInfo(origContent, modContent)
|
|
163
|
+
if (blocks.length === 0) return minPx
|
|
164
|
+
|
|
165
|
+
const counter = makeVisibleLineCounter(diffTotalLines, unchangedCtxLineNum, maxPx)
|
|
166
|
+
for (const block of blocks) {
|
|
167
|
+
counter.feed(block.start, block.end)
|
|
168
|
+
if (counter.maxReached) return maxPx
|
|
116
169
|
}
|
|
117
170
|
|
|
118
|
-
|
|
171
|
+
let result = counter.flush()
|
|
172
|
+
result = Math.max(minPx, result)
|
|
173
|
+
result = Math.min(maxPx, result)
|
|
174
|
+
|
|
175
|
+
return result
|
|
119
176
|
}
|
|
120
177
|
|
|
121
178
|
const height2Num = (heightStr: string): number => {
|
|
@@ -127,6 +184,7 @@ const height2Num = (heightStr: string): number => {
|
|
|
127
184
|
}
|
|
128
185
|
|
|
129
186
|
export const autoHeight = function ({
|
|
187
|
+
id,
|
|
130
188
|
patch,
|
|
131
189
|
pair,
|
|
132
190
|
maxHeight,
|
|
@@ -134,6 +192,7 @@ export const autoHeight = function ({
|
|
|
134
192
|
unchangedVisiable,
|
|
135
193
|
unchangedCtxLineNum,
|
|
136
194
|
}: {
|
|
195
|
+
id: string
|
|
137
196
|
patch?: string
|
|
138
197
|
pair?: WtoolDiffViewerProps['diffPair']
|
|
139
198
|
maxHeight: string
|
|
@@ -141,9 +200,19 @@ export const autoHeight = function ({
|
|
|
141
200
|
unchangedVisiable: boolean
|
|
142
201
|
unchangedCtxLineNum: number
|
|
143
202
|
}): number {
|
|
144
|
-
const
|
|
145
|
-
const
|
|
203
|
+
const cacheKey = unchangedVisiable ? 'visible' : 'hidden'
|
|
204
|
+
const cache = heightMap.get(id) || {}
|
|
205
|
+
const cachedHeight = cache?.[cacheKey]
|
|
206
|
+
if (cachedHeight !== undefined) return cachedHeight
|
|
207
|
+
|
|
208
|
+
const [minPx, maxPx] = [minHeight, maxHeight].map(height2Num)
|
|
209
|
+
const commonParams: CommonParams = { minPx, maxPx, unchangedVisiable, unchangedCtxLineNum }
|
|
210
|
+
const height = patch ? autoHeightPatch({ patch, ...commonParams }) : autoHeightPair({ pair, ...commonParams })
|
|
211
|
+
|
|
212
|
+
heightMap.set(id, {
|
|
213
|
+
...cache,
|
|
214
|
+
[cacheKey]: height,
|
|
215
|
+
})
|
|
146
216
|
|
|
147
|
-
|
|
148
|
-
return autoHeightPair({ pair, ...commonParams }) * LINE_HEIGHT
|
|
217
|
+
return height
|
|
149
218
|
}
|
|
@@ -1,58 +1,10 @@
|
|
|
1
|
+
import { parsePatchFilenames, parsePatchHunks } from '@/utils/patch'
|
|
2
|
+
|
|
1
3
|
export interface FilePair {
|
|
2
4
|
filename: string
|
|
3
5
|
content: string
|
|
4
6
|
}
|
|
5
7
|
|
|
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
8
|
/**
|
|
57
9
|
* 将 unified diff patch 转换为 [original, modified] 文件对。
|
|
58
10
|
*
|
|
@@ -71,7 +23,8 @@ export const patch2Pair = function (patch: string): FilePair[] {
|
|
|
71
23
|
]
|
|
72
24
|
}
|
|
73
25
|
|
|
74
|
-
const { origFilename, modFilename
|
|
26
|
+
const { origFilename, modFilename } = parsePatchFilenames(patch)
|
|
27
|
+
const hunks = parsePatchHunks(patch)
|
|
75
28
|
|
|
76
29
|
const origLines: string[] = []
|
|
77
30
|
const modLines: string[] = []
|
package/src/types.ts
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export interface Hunk {
|
|
2
|
+
origStart: number // original 起始行号(1-based)
|
|
3
|
+
origCount: number // original 行数
|
|
4
|
+
modStart: number // modified 起始行号(1-based)
|
|
5
|
+
modCount: number // modified 行数
|
|
6
|
+
lines: string[] // hunk 原始行(含前缀字符)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface PatchFilenames {
|
|
10
|
+
origFilename: string
|
|
11
|
+
modFilename: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 从 unified diff patch 解析 --- / +++ 行中的文件名。
|
|
16
|
+
* 文件名位于 patch 头部,遇到首个 @@ hunk 或已解析到两侧文件名后即停止扫描。
|
|
17
|
+
*/
|
|
18
|
+
export function parsePatchFilenames(patch: string): PatchFilenames {
|
|
19
|
+
let origFilename = ''
|
|
20
|
+
let modFilename = ''
|
|
21
|
+
|
|
22
|
+
let start = 0
|
|
23
|
+
while (start < patch.length) {
|
|
24
|
+
const nl = patch.indexOf('\n', start)
|
|
25
|
+
const line = nl === -1 ? patch.slice(start) : patch.slice(start, nl)
|
|
26
|
+
start = nl === -1 ? patch.length : nl + 1
|
|
27
|
+
|
|
28
|
+
if (line.startsWith('--- ')) {
|
|
29
|
+
origFilename = line.slice(4).trim()
|
|
30
|
+
if (origFilename && modFilename) break
|
|
31
|
+
continue
|
|
32
|
+
}
|
|
33
|
+
if (line.startsWith('+++ ')) {
|
|
34
|
+
modFilename = line.slice(4).trim()
|
|
35
|
+
if (origFilename && modFilename) break
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
if (line.startsWith('@@')) break
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return { origFilename, modFilename }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 从 unified diff patch 解析所有 hunk(@@ 块)
|
|
46
|
+
*/
|
|
47
|
+
export function parsePatchHunks(patch: string): Hunk[] {
|
|
48
|
+
const lines = patch.split('\n')
|
|
49
|
+
|
|
50
|
+
const hunks: Hunk[] = []
|
|
51
|
+
let currentHunk: Hunk | null = null
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
// @@ -origStart,origCount +modStart,modCount @@
|
|
55
|
+
const hunkMatch = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@/)
|
|
56
|
+
if (hunkMatch) {
|
|
57
|
+
currentHunk = {
|
|
58
|
+
origStart: parseInt(hunkMatch[1], 10),
|
|
59
|
+
origCount: hunkMatch[2] !== undefined ? parseInt(hunkMatch[2], 10) : 1,
|
|
60
|
+
modStart: parseInt(hunkMatch[3], 10),
|
|
61
|
+
modCount: hunkMatch[4] !== undefined ? parseInt(hunkMatch[4], 10) : 1,
|
|
62
|
+
lines: [],
|
|
63
|
+
}
|
|
64
|
+
hunks.push(currentHunk)
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (currentHunk) {
|
|
68
|
+
if (line.startsWith('\\')) continue // ""
|
|
69
|
+
currentHunk.lines.push(line)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return hunks
|
|
74
|
+
}
|