Skip to content

Commit 6fa2142

Browse files
committed
fix: justified diff chunk
1 parent 0177ff4 commit 6fa2142

File tree

4 files changed

+259
-212
lines changed

4 files changed

+259
-212
lines changed

src/App.vue

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
<template>
22
<div id="app">
33
<img alt="Vue logo" src="./assets/logo.png">
4-
<diff-viewer :new-str="newStr" :old-str="oldStr" title="测试删除"/>
5-
<diff-viewer :new-str="oldStr" :old-str="newStr" title="测试添加"/>
6-
<diff-viewer :new-str="modifiedStr" :old-str="oldStr" title="测试修改"/>
4+
<code-diff-viewer :new-content="newStr" :old-content="oldStr" title="测试删除"/>
5+
<code-diff-viewer :new-content="oldStr" :old-content="newStr" title="测试添加"/>
6+
<code-diff-viewer :new-content="modifiedStr" :old-content="oldStr" title="测试修改"/>
77
</div>
88
</template>
99

1010
<script>
11-
import DiffViewer from './components/DiffViewer.vue';
11+
import CodeDiffViewer from './components/CodeDiffViewer.vue';
1212
1313
export default {
1414
name: 'app',
1515
components: {
16-
DiffViewer
16+
CodeDiffViewer
1717
},
1818
data() {
1919
return {

src/components/DiffChunk.vue renamed to src/components/CodeDiffChunk.vue

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
<div class="diff-chunk">
33
<div v-if="chunk.collapse"
44
class="collapse-tip"
5-
@click.once="$emit('expand', index)">
5+
@click.once="$emit('expand', chunk.leftIndex, chunk.rightIndex)">
66
... 隐藏 {{chunk.lineCount}} 行,点击展开 ...
77
</div>
88
<div v-else class="line"
99
v-for="(line, index) in chunk.lines"
1010
:key="index">
1111
<span v-if="chunk.type !== 'blank'"
12-
:class="chunk.type">{{chunk.startCount + index}}</span>
12+
:class="chunk.type">{{chunk.startLineNumber + index}}</span>
1313
<span v-else></span>
1414
<pre :class="chunk.type">{{line}}</pre>
1515
</div>
@@ -18,15 +18,12 @@
1818

1919
<script>
2020
export default {
21-
name: 'diff-chunk',
21+
name: 'code-diff-chunk',
2222
props: {
2323
chunk: {
2424
type: Object,
25-
default: () => {
26-
return {};
27-
}
28-
},
29-
index: Number
25+
default: () => {}
26+
}
3027
}
3128
};
3229
</script>
@@ -37,17 +34,17 @@ export default {
3734
display: flex;
3835
3936
span {
40-
width: 48px;
41-
padding-left: 10px;
42-
padding-right: 10px;
37+
width: 40px;
38+
padding-left: 5px;
39+
padding-right: 8px;
4340
line-height: 22px;
4441
box-sizing: border-box;
4542
text-align: right;
4643
}
4744
pre {
4845
display: inline-block;
4946
margin: 0;
50-
padding-left: 25px;
47+
padding-left: 20px;
5148
width: 100%;
5249
line-height: 22px;
5350
word-wrap: break-word;

src/components/CodeDiffViewer.vue

Lines changed: 245 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,245 @@
1+
<template>
2+
<div class="diff-viewer">
3+
<h3>{{title}}</h3>
4+
<div class="container" v-if="newContent && oldContent">
5+
<div class="left">
6+
<code-chunk v-for="(chunk, index) in splitedLeft"
7+
:key="index"
8+
:chunk="chunk" @expand="expandChunk"/>
9+
</div>
10+
<div class="right">
11+
<code-chunk v-for="(chunk, index) in splitedRight"
12+
:key="index"
13+
:chunk="chunk" @expand="expandChunk"/>
14+
</div>
15+
</div>
16+
<div v-else v-for="(chunk, index) in unifiedResult"
17+
:key="index">
18+
<code-chunk :chunk="chunk" :index="index"/>
19+
</div>
20+
</div>
21+
</template>
22+
23+
<script>
24+
import {diffLines} from 'diff/lib/diff/line';
25+
import CodeChunk from './CodeDiffChunk';
26+
import {clone} from 'lodash';
27+
28+
export default {
29+
name: 'code-iff-viewer',
30+
components: {
31+
CodeChunk
32+
},
33+
props: {
34+
oldContent: String,
35+
newContent: String,
36+
title: String,
37+
collapse: {
38+
type: Number,
39+
default: 10 // 连续超过10行没改动,默认折叠
40+
}
41+
},
42+
data() {
43+
return {
44+
unifiedResult: [],
45+
splitedLeft: [],
46+
splitedRight: []
47+
};
48+
},
49+
created() {
50+
this.calculateDiff();
51+
},
52+
watch: {
53+
'title'(v) {
54+
this.calculateDiff();
55+
}
56+
},
57+
methods: {
58+
calculateDiff() {
59+
this.unifiedResult = this.diff();
60+
this.splitedLeft = [];
61+
this.splitedRight = [];
62+
if (this.unifiedResult.length) {
63+
let {left, right} = this.splitDiffResult(this.unifiedResult);
64+
this.adaptSplitResult(left, right);
65+
}
66+
},
67+
diff() {
68+
// 修改
69+
if (this.newContent && this.oldContent) {
70+
let diffs = diffLines(this.oldContent, this.newContent);
71+
return diffs.map((chunk, index) => {
72+
let type = chunk.added ? 'add' : (chunk.removed ? 'remove' : '');
73+
let lines = chunk.value.split('\n');
74+
let lineCount = lines.length;
75+
let collapse = !type && lineCount > this.collapse;
76+
return {
77+
type,
78+
lines,
79+
lineCount,
80+
collapse
81+
};
82+
});
83+
}
84+
else if (this.newContent || this.oldContent) {
85+
// 新增 or 删除
86+
let diffs = this.newContent || this.oldContent;
87+
let lines = diffs.split('\n');
88+
let type = !this.newContent ? 'remove' : (!this.oldContent ? 'add' : '');
89+
return [{
90+
type,
91+
lines,
92+
lineCount: lines.length,
93+
startCount: 1
94+
}];
95+
}
96+
return [];
97+
},
98+
99+
/**
100+
* 由于 diff库返回的结果是 chunk list非双边对比结果,所以这里做了处理
101+
* 1. 新增块放入右侧,如果下一块为删除块则视为更改代码,此时左侧需补充空行。
102+
* 2. 删除块放入左侧,如果下一块为新增块则视为更改代码, 此时右侧补充空行。
103+
* 3. 无变化块,两边都需放入。
104+
*
105+
* @param {Array} diffResult diff库调用返回的结果
106+
* @return {Object} {left, right} 左右分离结果
107+
*/
108+
splitDiffResult(diffResult) {
109+
let left = {
110+
chunks: [],
111+
lineCount: 1
112+
};
113+
let right = {
114+
chunks: [],
115+
lineCount: 1
116+
};
117+
let setChunkLineNumber = (chunk, lineNumer) => {
118+
chunk.startLineNumber = lineNumer;
119+
return lineNumer + chunk.lineCount;
120+
};
121+
122+
diffResult.forEach((chunk, index) => {
123+
if (chunk.type === 'add') {
124+
right.lineCount = setChunkLineNumber(chunk, right.lineCount);
125+
right.chunks.push(chunk);
126+
// 判断是否增加空白块
127+
if (this.shouldSetBlank(chunk.type, index)) {
128+
left.chunks.push(this.createBlankChunk(chunk.lineCount));
129+
}
130+
}
131+
else if (chunk.type === 'remove') {
132+
left.lineCount = setChunkLineNumber(chunk, left.lineCount);
133+
left.chunks.push(chunk);
134+
// 判断是否增加空白块
135+
if (this.shouldSetBlank(chunk.type, index)) {
136+
right.chunks.push(this.createBlankChunk(chunk.lineCount));
137+
}
138+
}
139+
else {
140+
// 没有变动,两边放入。
141+
left.lineCount = setChunkLineNumber(chunk, left.lineCount);
142+
left.chunks.push(chunk);
143+
144+
let clonedChunk = clone(chunk);
145+
right.lineCount = setChunkLineNumber(clonedChunk, right.lineCount);
146+
right.chunks.push(chunk);
147+
}
148+
});
149+
return {left, right};
150+
},
151+
adaptSplitResult(left, right) {
152+
left.chunks.forEach((leftChunk, index) => {
153+
let rightChunk = right.chunks[index];
154+
if (leftChunk.collapse && rightChunk.collapse) {
155+
// 记录下左右栏chunk index; 点击展开时,通过此index定位chunk进行展开。
156+
leftChunk.leftIndex = rightChunk.leftIndex = this.splitedLeft.length;
157+
leftChunk.rightIndex = rightChunk.rightIndex = this.splitedRight.length;
158+
}
159+
160+
this.splitedLeft.push(leftChunk);
161+
this.splitedRight.push(rightChunk);
162+
163+
// 修改的行数不一致时,补充空白块。例如:左栏删除 3行,右栏添加5行代码。则左栏需补充 2行空白,进行对齐。
164+
if (leftChunk.type === 'remove' && rightChunk.type === 'add') {
165+
let count = leftChunk.lineCount - rightChunk.lineCount;
166+
if (count < 0) {
167+
this.splitedLeft.push(this.createBlankChunk(Math.abs(count)));
168+
}
169+
else if (count > 0) {
170+
this.splitedRight.push(this.createBlankChunk(count));
171+
}
172+
}
173+
});
174+
},
175+
176+
/**
177+
* 是否应该设置空白块:
178+
* 1. 最后一块返回 true
179+
* 2. 当前块是‘remove'类型,看下一块类型是否为空(没有变化)
180+
* 3. 当前块是‘add'类型,看上一块类型是否为空(没有变化)
181+
*
182+
* @param {type} type 当前块类型
183+
* @param {inddex} index 当前块索引
184+
* @return {boolean}
185+
*/
186+
shouldSetBlank(type, index) {
187+
if (index === this.unifiedResult.length - 1) {
188+
return true;
189+
}
190+
index = type === 'remove' ? index + 1 : index - 1;
191+
192+
let chunk = this.unifiedResult[index];
193+
return !chunk || !chunk.type;
194+
},
195+
196+
/**
197+
* 创建空白块
198+
*
199+
* @param {number} lineCount 行数
200+
* @return {Object} 空白块对象
201+
*/
202+
createBlankChunk(lineCount) {
203+
return {
204+
type: 'blank',
205+
lineCount,
206+
lines: new Array(lineCount).fill(' ')
207+
};
208+
},
209+
expandChunk(leftIndex, rightIndex) {
210+
this.splitedLeft[leftIndex].collapse = false;
211+
this.splitedRight[rightIndex].collapse = false;
212+
}
213+
}
214+
};
215+
</script>
216+
217+
<style lang="less" scoped>
218+
.diff-viewer {
219+
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace, sans-serif;
220+
font-size: 12px;
221+
margin-bottom: 15px;
222+
border-radius: 5px;
223+
border: 1px solid #ddd;
224+
225+
h3 {
226+
box-sizing: border-box;
227+
margin: 0;
228+
padding: 5px 10px;
229+
height: 40px;
230+
line-height: 30px;
231+
border-radius: 5px 5px 0 0;
232+
border-bottom: 1px solid #dbdbdb;
233+
background-color: #f7f7f7;
234+
color: #333;
235+
font-weight: 600;
236+
}
237+
238+
.container {
239+
display: flex;
240+
> div {
241+
width: 50%;
242+
}
243+
}
244+
}
245+
</style>

0 commit comments

Comments
 (0)