@yuhufe/wtool-vdiff 0.0.6 → 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 +3108 -2918
- 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 +86 -24
- 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,8 +1,10 @@
|
|
|
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
|
|
5
6
|
const GAP_HEIGHT = 24
|
|
7
|
+
const heightMap = new Map<string, Partial<Record<'visible' | 'hidden', number>>>()
|
|
6
8
|
|
|
7
9
|
interface CommonParams {
|
|
8
10
|
minPx: number
|
|
@@ -11,6 +13,54 @@ interface CommonParams {
|
|
|
11
13
|
unchangedCtxLineNum: number
|
|
12
14
|
}
|
|
13
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
|
+
|
|
14
64
|
/**
|
|
15
65
|
* 在线可见行计数器,按顺序逐个喂入变更区间(1-based inclusive),流式累计可见行数。
|
|
16
66
|
* 区间必须按 start 升序喂入,每次 feed 后可读取 maxReached 判断是否达到像素上限。
|
|
@@ -19,10 +69,10 @@ interface CommonParams {
|
|
|
19
69
|
* @param maxPx 像素上限,达到后 maxReached 为 true,调用方应立即停止 feed
|
|
20
70
|
*/
|
|
21
71
|
function makeVisibleLineCounter(totalLines: number, ctx: number, maxPx: number) {
|
|
22
|
-
let visible = 0
|
|
23
|
-
let gaps = 0
|
|
72
|
+
let visible = 0 // 已 commit 的确定可见行数(不含当前 pending 块)
|
|
73
|
+
let gaps = 0 // gap widget 数量(每个折叠区域占 GAP_HEIGHT,非 LINE_HEIGHT)
|
|
24
74
|
let pendingStart = -1 // 当前待合并窗口的起始行(-1 表示无 pending)
|
|
25
|
-
let pendingEnd = -1
|
|
75
|
+
let pendingEnd = -1 // 当前待合并窗口的结束行
|
|
26
76
|
|
|
27
77
|
const usedPx = () => visible * LINE_HEIGHT + gaps * GAP_HEIGHT
|
|
28
78
|
|
|
@@ -39,7 +89,7 @@ function makeVisibleLineCounter(totalLines: number, ctx: number, maxPx: number)
|
|
|
39
89
|
const winStart = Math.max(1, s - ctx)
|
|
40
90
|
const winEnd = Math.min(totalLines, e + ctx)
|
|
41
91
|
if (pendingEnd === -1 || winStart > pendingEnd + 1) {
|
|
42
|
-
commitPending()
|
|
92
|
+
commitPending() // 新窗口与 pending 有间隙:先提交 pending,再开新窗口
|
|
43
93
|
pendingStart = winStart
|
|
44
94
|
pendingEnd = winEnd
|
|
45
95
|
} else {
|
|
@@ -72,14 +122,11 @@ const autoHeightPatch = function ({
|
|
|
72
122
|
}: {
|
|
73
123
|
patch: string
|
|
74
124
|
} & CommonParams): number {
|
|
75
|
-
const
|
|
125
|
+
const hunks = parsePatchHunks(patch)
|
|
76
126
|
if (hunks.length === 0) return minPx
|
|
77
127
|
|
|
78
128
|
const lastHunk = hunks[hunks.length - 1]
|
|
79
|
-
const totalLines = Math.max(
|
|
80
|
-
lastHunk.origStart + lastHunk.origCount - 1,
|
|
81
|
-
lastHunk.modStart + lastHunk.modCount - 1,
|
|
82
|
-
)
|
|
129
|
+
const totalLines = Math.max(lastHunk.origStart + lastHunk.origCount - 1, lastHunk.modStart + lastHunk.modCount - 1)
|
|
83
130
|
const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, maxPx)
|
|
84
131
|
|
|
85
132
|
for (const h of hunks) {
|
|
@@ -102,27 +149,30 @@ const autoHeightPair = function ({
|
|
|
102
149
|
} & CommonParams): number {
|
|
103
150
|
if (!pair || pair.length < 2) return minPx
|
|
104
151
|
|
|
105
|
-
const
|
|
106
|
-
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')
|
|
107
156
|
const totalLines = Math.max(origLines.length, modLines.length)
|
|
108
157
|
|
|
109
158
|
if (unchangedVisiable) {
|
|
110
159
|
return Math.max(minPx, Math.min(totalLines * LINE_HEIGHT, maxPx))
|
|
111
160
|
}
|
|
112
161
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
while (lo < origLines.length && lo < modLines.length && origLines[lo] === modLines[lo]) lo++
|
|
162
|
+
const { blocks, totalLines: diffTotalLines } = getChangedLineInfo(origContent, modContent)
|
|
163
|
+
if (blocks.length === 0) return minPx
|
|
116
164
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
169
|
+
}
|
|
120
170
|
|
|
121
|
-
|
|
171
|
+
let result = counter.flush()
|
|
172
|
+
result = Math.max(minPx, result)
|
|
173
|
+
result = Math.min(maxPx, result)
|
|
122
174
|
|
|
123
|
-
|
|
124
|
-
counter.feed(lo + 1, Math.max(origHi, modHi))
|
|
125
|
-
return Math.max(minPx, counter.flush())
|
|
175
|
+
return result
|
|
126
176
|
}
|
|
127
177
|
|
|
128
178
|
const height2Num = (heightStr: string): number => {
|
|
@@ -134,6 +184,7 @@ const height2Num = (heightStr: string): number => {
|
|
|
134
184
|
}
|
|
135
185
|
|
|
136
186
|
export const autoHeight = function ({
|
|
187
|
+
id,
|
|
137
188
|
patch,
|
|
138
189
|
pair,
|
|
139
190
|
maxHeight,
|
|
@@ -141,6 +192,7 @@ export const autoHeight = function ({
|
|
|
141
192
|
unchangedVisiable,
|
|
142
193
|
unchangedCtxLineNum,
|
|
143
194
|
}: {
|
|
195
|
+
id: string
|
|
144
196
|
patch?: string
|
|
145
197
|
pair?: WtoolDiffViewerProps['diffPair']
|
|
146
198
|
maxHeight: string
|
|
@@ -148,9 +200,19 @@ export const autoHeight = function ({
|
|
|
148
200
|
unchangedVisiable: boolean
|
|
149
201
|
unchangedCtxLineNum: number
|
|
150
202
|
}): number {
|
|
203
|
+
const cacheKey = unchangedVisiable ? 'visible' : 'hidden'
|
|
204
|
+
const cache = heightMap.get(id) || {}
|
|
205
|
+
const cachedHeight = cache?.[cacheKey]
|
|
206
|
+
if (cachedHeight !== undefined) return cachedHeight
|
|
207
|
+
|
|
151
208
|
const [minPx, maxPx] = [minHeight, maxHeight].map(height2Num)
|
|
152
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
|
+
})
|
|
153
216
|
|
|
154
|
-
|
|
155
|
-
return autoHeightPair({ pair, ...commonParams })
|
|
217
|
+
return height
|
|
156
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
|
+
}
|