@yuhufe/wtool-vdiff 0.0.7 → 0.0.8
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 +140 -41
- package/dist/DiffFiles/DiffFiles.vue.d.ts +12 -0
- package/dist/DiffFiles/DiffList/DiffList.vue.d.ts +94 -0
- package/dist/DiffFiles/FileExplore/FileExplore.vue.d.ts +510 -0
- package/dist/DiffFiles/FileExplore/FileSearch.vue.d.ts +11 -0
- package/dist/DiffFiles/FileExplore/fileTree.d.ts +17 -0
- package/dist/DiffFiles/createDiffFiles.d.ts +13 -0
- package/dist/DiffFiles/index.d.ts +3 -0
- package/dist/DiffFiles/types.d.ts +12 -0
- package/dist/DiffFiles/useDiffFiles.d.ts +23 -0
- package/dist/DiffViewer/DiffViewer.vue.d.ts +15 -4
- package/dist/DiffViewer/TopBar.vue.d.ts +2 -0
- package/dist/DiffViewer/const.d.ts +7 -0
- package/dist/DiffViewer/utils/autoHeight.d.ts +3 -1
- package/dist/DiffViewer/utils/patch2Pair.d.ts +10 -0
- package/dist/index.d.ts +1 -0
- package/dist/types.d.ts +15 -5
- package/dist/wtool-vdiff.cjs.js +42 -39
- package/dist/wtool-vdiff.es.js +9306 -5945
- package/package.json +6 -7
- package/src/DiffFiles/DiffFiles.vue +97 -18
- package/src/DiffFiles/DiffList/DiffList.vue +125 -0
- package/src/DiffFiles/FileExplore/FileExplore.vue +165 -7
- package/src/DiffFiles/FileExplore/FileSearch.vue +81 -0
- package/src/DiffFiles/FileExplore/fileTree.ts +102 -0
- package/src/DiffFiles/createDiffFiles.ts +70 -0
- package/src/DiffFiles/index.ts +11 -0
- package/src/DiffFiles/types.ts +15 -0
- package/src/DiffFiles/useDiffFiles.ts +56 -0
- package/src/DiffViewer/DiffViewer.vue +22 -8
- package/src/DiffViewer/MonacoDiffViewer.vue +21 -2
- package/src/DiffViewer/TopBar.vue +23 -4
- package/src/DiffViewer/const.ts +10 -0
- package/src/DiffViewer/utils/autoHeight.ts +83 -29
- package/src/DiffViewer/utils/patch2Pair.ts +37 -9
- package/src/assets/file.svg +4 -0
- package/src/assets/folder.svg +4 -0
- package/src/index.ts +1 -0
- package/src/types.ts +17 -5
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { defineCustomElement } from 'vue'
|
|
2
|
+
import type { WtoolDiffFilesProps } from '../types'
|
|
3
|
+
import DiffFiles from './DiffFiles.vue'
|
|
4
|
+
import type { DiffFileSelection } from './FileExplore/fileTree'
|
|
5
|
+
|
|
6
|
+
export type DiffFilesProps = WtoolDiffFilesProps
|
|
7
|
+
export type DiffFilesSelectFileListener = (selection: DiffFileSelection) => void
|
|
8
|
+
|
|
9
|
+
export const WTOOL_DIFF_FILES_TAG = 'wtool-diff-files'
|
|
10
|
+
|
|
11
|
+
export const WtoolDiffFiles = defineCustomElement(DiffFiles, {
|
|
12
|
+
shadowRoot: false,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
export function register(tagName: string = WTOOL_DIFF_FILES_TAG): void {
|
|
16
|
+
if (customElements.get(tagName)) return
|
|
17
|
+
customElements.define(tagName, WtoolDiffFiles)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface DiffFilesInstance {
|
|
21
|
+
update(props: Partial<DiffFilesProps>): void
|
|
22
|
+
onSelectFile(listener: DiffFilesSelectFileListener): () => void
|
|
23
|
+
destroy(): void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyProps(el: InstanceType<typeof WtoolDiffFiles>, props: Partial<DiffFilesProps>): void {
|
|
27
|
+
const node = el as unknown as Record<string, unknown>
|
|
28
|
+
for (const key of Object.keys(props) as (keyof DiffFilesProps)[]) {
|
|
29
|
+
const value = props[key]
|
|
30
|
+
if (value !== undefined) {
|
|
31
|
+
node[key as string] = value
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createDiffFiles(target: HTMLElement, initialProps: DiffFilesProps = {}): DiffFilesInstance {
|
|
37
|
+
register()
|
|
38
|
+
const el = new WtoolDiffFiles()
|
|
39
|
+
const selectFileListeners = new Set<EventListener>()
|
|
40
|
+
|
|
41
|
+
applyProps(el, initialProps)
|
|
42
|
+
target.appendChild(el)
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
update(newProps: Partial<DiffFilesProps>) {
|
|
46
|
+
applyProps(el, newProps)
|
|
47
|
+
},
|
|
48
|
+
onSelectFile(listener: DiffFilesSelectFileListener) {
|
|
49
|
+
const eventListener: EventListener = event => {
|
|
50
|
+
const [selection] = (event as CustomEvent<[DiffFileSelection]>).detail
|
|
51
|
+
listener(selection)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
selectFileListeners.add(eventListener)
|
|
55
|
+
el.addEventListener('select-file', eventListener)
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
selectFileListeners.delete(eventListener)
|
|
59
|
+
el.removeEventListener('select-file', eventListener)
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
destroy() {
|
|
63
|
+
selectFileListeners.forEach(listener => {
|
|
64
|
+
el.removeEventListener('select-file', listener)
|
|
65
|
+
})
|
|
66
|
+
selectFileListeners.clear()
|
|
67
|
+
el.remove()
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export {
|
|
2
|
+
WTOOL_DIFF_FILES_TAG,
|
|
3
|
+
WtoolDiffFiles,
|
|
4
|
+
register as registerDiffFiles,
|
|
5
|
+
createDiffFiles,
|
|
6
|
+
type DiffFilesProps,
|
|
7
|
+
type DiffFilesInstance,
|
|
8
|
+
type DiffFilesSelectFileListener,
|
|
9
|
+
} from './createDiffFiles'
|
|
10
|
+
export type { DiffFileSelection } from './FileExplore/fileTree'
|
|
11
|
+
export type { DiffFile, FileTree, WtoolDiffFilesProps, WtoolDiffViewerStyle } from '../types'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DiffFile } from '../types'
|
|
2
|
+
|
|
3
|
+
// 文件列表的每个文件类型
|
|
4
|
+
export interface FileItem extends DiffFile {
|
|
5
|
+
fullPath: string
|
|
6
|
+
name: string
|
|
7
|
+
folderPath: string
|
|
8
|
+
type?: string
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface DiffFileState {
|
|
12
|
+
height: number
|
|
13
|
+
viewed: boolean
|
|
14
|
+
rawed: boolean
|
|
15
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useCompExp } from '@yuhufe/web-ui'
|
|
2
|
+
import type { Ref } from 'vue'
|
|
3
|
+
|
|
4
|
+
import { DiffFile } from '@/types'
|
|
5
|
+
import { parsePatchFilenames } from '@/utils/patch'
|
|
6
|
+
import type { FileItem } from './types'
|
|
7
|
+
|
|
8
|
+
export const useDiffFiles = function ({ isMaster = false } = {}) {
|
|
9
|
+
const exp = useCompExp<{
|
|
10
|
+
selectedFileKey: Ref<string>
|
|
11
|
+
searchKeyword: Ref<string>
|
|
12
|
+
selectFile: (fullPath: string) => Promise<void>
|
|
13
|
+
filterTree: (keyword: string) => void
|
|
14
|
+
}>({ isMaster, key: 'diffFiles' })
|
|
15
|
+
|
|
16
|
+
return { ...exp }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatDiffFiles(diffFiles: DiffFile[]) {
|
|
20
|
+
function resolveDiffFileFullPath(file: DiffFile): string {
|
|
21
|
+
if (file.fullPath) return file.fullPath
|
|
22
|
+
|
|
23
|
+
if (file.diffPair) {
|
|
24
|
+
return file.diffPair[1]?.filename || file.diffPair[0]?.filename
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (typeof file.diffPatch === 'string') {
|
|
28
|
+
return parsePatchFilenames(file.diffPatch).modFilename
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return ''
|
|
32
|
+
}
|
|
33
|
+
const files = diffFiles.map<FileItem>((file, index) => {
|
|
34
|
+
const fullPath = resolveDiffFileFullPath(file)
|
|
35
|
+
|
|
36
|
+
const pathSegments = fullPath.split('/')
|
|
37
|
+
const name = pathSegments.at(-1) || ''
|
|
38
|
+
const extname = name.split('.').at(-1)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...file,
|
|
42
|
+
fullPath,
|
|
43
|
+
name,
|
|
44
|
+
folderPath: pathSegments.slice(0, -1).join('/'),
|
|
45
|
+
type: extname?.toLowerCase(),
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
files.sort((left, right) => {
|
|
50
|
+
if (left.fullPath === right.fullPath) return 0
|
|
51
|
+
return left.fullPath > right.fullPath ? 1 : -1
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const fileMap: Record<string, FileItem> = Object.fromEntries(files.map(file => [file.fullPath, file]))
|
|
55
|
+
return { files, fileMap }
|
|
56
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="diff-viewer-wrap" :style="viewerStyle">
|
|
3
|
-
<TopBar class="top-bar" :diffPair="diffPair" />
|
|
2
|
+
<div class="diff-viewer-wrap" :style="viewerStyle" @viewStateChange="">
|
|
3
|
+
<TopBar class="top-bar" :diffPair="diffPair" :diffPatch="diffPatch" />
|
|
4
4
|
<div class="content-wrap" v-show="!viewed" v-loading="loading">
|
|
5
5
|
<MonacoDiffViewer
|
|
6
6
|
class="monaco-container"
|
|
@@ -29,9 +29,12 @@ import { patch2Pair } from './utils/patch2Pair'
|
|
|
29
29
|
import { autoHeight } from './utils/autoHeight'
|
|
30
30
|
import { HEIGHT_TOP_BAR } from './const'
|
|
31
31
|
|
|
32
|
+
const emit = defineEmits<{
|
|
33
|
+
viewStateChange: [data: { viewed: boolean; rawed: boolean }]
|
|
34
|
+
}>()
|
|
35
|
+
|
|
32
36
|
const vLoading = loadingDirective
|
|
33
37
|
const loading = ref(true)
|
|
34
|
-
const autoHeightId = v4()
|
|
35
38
|
|
|
36
39
|
const _renderStart = performance.now()
|
|
37
40
|
const onMonacoRenderComplete = async () => {
|
|
@@ -41,6 +44,7 @@ const onMonacoRenderComplete = async () => {
|
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
const props = withDefaults(defineProps<WtoolDiffViewerProps>(), {
|
|
47
|
+
fileId: '',
|
|
44
48
|
diffPair: () => [],
|
|
45
49
|
diffPatch: '',
|
|
46
50
|
language: 'plaintext',
|
|
@@ -92,8 +96,8 @@ const mergedOptions = computed(() => {
|
|
|
92
96
|
}
|
|
93
97
|
})
|
|
94
98
|
|
|
95
|
-
const viewed = ref<boolean>(false) // 是否已读
|
|
96
|
-
const rawed = ref<boolean>(false) // 是否显示原始文件
|
|
99
|
+
const viewed = ref<boolean>(props.viewerStyle?.viewed ?? false) // 是否已读
|
|
100
|
+
const rawed = ref<boolean>(props.viewerStyle?.rawed ?? false) // 是否显示原始文件
|
|
97
101
|
const canUnchangeVisible = ref(!props.diffPatch) // patch 模式下未改动区域为空行,不可展示
|
|
98
102
|
|
|
99
103
|
// 编辑器样式
|
|
@@ -108,17 +112,19 @@ const viewerHeight = computed(() => {
|
|
|
108
112
|
...(props.viewerStyle || {}),
|
|
109
113
|
}
|
|
110
114
|
|
|
115
|
+
const autoHeightId = props.fileId || v4()
|
|
111
116
|
const height = autoHeight({
|
|
112
117
|
id: autoHeightId,
|
|
113
118
|
patch: props.diffPatch,
|
|
114
119
|
pair: props.diffPair,
|
|
115
120
|
...heightRange,
|
|
116
121
|
unchangedVisiable: funcs.rawed.value,
|
|
117
|
-
unchangedCtxLineNum: mergedOptions.value.hideUnchangedRegions.contextLineCount
|
|
122
|
+
unchangedCtxLineNum: mergedOptions.value.hideUnchangedRegions.contextLineCount ?? 3,
|
|
123
|
+
unchangedMinLineNum: mergedOptions.value.hideUnchangedRegions.minimumLineCount ?? 1,
|
|
118
124
|
})
|
|
119
125
|
|
|
120
|
-
//
|
|
121
|
-
return `${height
|
|
126
|
+
// 当前高度为代码高度
|
|
127
|
+
return `${height}px`
|
|
122
128
|
})
|
|
123
129
|
const viewerStyle = computed(() => {
|
|
124
130
|
return {
|
|
@@ -129,7 +135,15 @@ const viewerStyle = computed(() => {
|
|
|
129
135
|
|
|
130
136
|
registerFunc({
|
|
131
137
|
viewed,
|
|
138
|
+
updateViewed(newVal) {
|
|
139
|
+
viewed.value = newVal
|
|
140
|
+
emit('viewStateChange', { rawed: rawed.value, viewed: newVal })
|
|
141
|
+
},
|
|
132
142
|
rawed,
|
|
143
|
+
updateRawed(newVal) {
|
|
144
|
+
rawed.value = newVal
|
|
145
|
+
emit('viewStateChange', { rawed: newVal, viewed: viewed.value })
|
|
146
|
+
},
|
|
133
147
|
canUnchangeVisible,
|
|
134
148
|
})
|
|
135
149
|
</script>
|
|
@@ -7,6 +7,7 @@ import { ref, watch, onMounted, onBeforeUnmount, shallowRef } from 'vue'
|
|
|
7
7
|
import loader from '@monaco-editor/loader'
|
|
8
8
|
import type * as Monaco from 'monaco-editor'
|
|
9
9
|
import { useDiffViewer } from './useDiffView'
|
|
10
|
+
import { HEIGHT_CODE_LINE, HEIGHT_HORIZONTAL_SCROLLBAR } from './const'
|
|
10
11
|
|
|
11
12
|
type DiffEditorOptions = Monaco.editor.IStandaloneDiffEditorConstructionOptions
|
|
12
13
|
type ModelOptions = Monaco.editor.ITextModelUpdateOptions
|
|
@@ -58,15 +59,18 @@ onMounted(() => {
|
|
|
58
59
|
renderSideBySide: true,
|
|
59
60
|
useInlineViewWhenSpaceIsLimited: false,
|
|
60
61
|
scrollBeyondLastLine: false,
|
|
62
|
+
lineHeight: HEIGHT_CODE_LINE,
|
|
61
63
|
hideUnchangedRegions: {
|
|
62
64
|
enabled: true,
|
|
63
65
|
contextLineCount: 3,
|
|
64
66
|
},
|
|
67
|
+
...props.options,
|
|
65
68
|
scrollbar: {
|
|
66
69
|
verticalScrollbarSize: 8,
|
|
67
|
-
horizontalScrollbarSize:
|
|
70
|
+
horizontalScrollbarSize: HEIGHT_HORIZONTAL_SCROLLBAR,
|
|
71
|
+
alwaysConsumeMouseWheel: false,
|
|
72
|
+
...props.options.scrollbar,
|
|
68
73
|
},
|
|
69
|
-
...props.options,
|
|
70
74
|
})
|
|
71
75
|
editor.value.setModel({
|
|
72
76
|
original: originalModel.value,
|
|
@@ -165,6 +169,21 @@ watch(
|
|
|
165
169
|
</style>
|
|
166
170
|
|
|
167
171
|
<style>
|
|
172
|
+
/* Monaco tooltip 会挂载到独立的 context view 中,需使用非 scoped 样式避免遮挡工具按钮 */
|
|
173
|
+
.context-view.monaco-component {
|
|
174
|
+
.workbench-hover-container {
|
|
175
|
+
.hover-contents {
|
|
176
|
+
white-space: nowrap !important;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
.monaco-editor-container {
|
|
182
|
+
.codicon.codicon-widget-close {
|
|
183
|
+
box-sizing: content-box !important;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
168
187
|
/* patch 模式:未改动区域为空行,隐藏 monaco 内置的展开未改动区域按钮(非 scoped,配合 JS class 控制) */
|
|
169
188
|
.monaco-editor-container.hide-unchanged-actions {
|
|
170
189
|
.diff-hidden-lines-widget {
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
<div class="filename">{{ filenameDisplay }}</div>
|
|
5
5
|
<span :class="['diff-type-tag', diffType]">{{ diffTypeLabel }}</span>
|
|
6
6
|
<div class="diff-line-num">
|
|
7
|
-
<div class="add" v-if="diffType !== 'del'">+{{
|
|
8
|
-
<div class="del" v-if="diffType !== 'add'">-{{
|
|
7
|
+
<div class="add" v-if="diffType !== 'del'">+{{ displayedChanged.added }}</div>
|
|
8
|
+
<div class="del" v-if="diffType !== 'add'">-{{ displayedChanged.removed }}</div>
|
|
9
9
|
</div>
|
|
10
10
|
</div>
|
|
11
11
|
<div class="toolbar">
|
|
@@ -25,15 +25,18 @@
|
|
|
25
25
|
import { computed, ref, onMounted } from 'vue'
|
|
26
26
|
import { useDiffViewer } from './useDiffView'
|
|
27
27
|
import { HEIGHT_TOP_BAR } from './const'
|
|
28
|
+
import { parsePatchHunks } from '@/utils/patch'
|
|
28
29
|
|
|
29
30
|
const { funcs, registerFunc } = useDiffViewer()
|
|
30
31
|
|
|
31
32
|
const props = withDefaults(
|
|
32
33
|
defineProps<{
|
|
33
34
|
diffPair?: { filename: string; content: string | null }[]
|
|
35
|
+
diffPatch?: string
|
|
34
36
|
}>(),
|
|
35
37
|
{
|
|
36
38
|
diffPair: () => [],
|
|
39
|
+
diffPatch: '',
|
|
37
40
|
}
|
|
38
41
|
)
|
|
39
42
|
|
|
@@ -74,11 +77,11 @@ const filenameDisplay = computed(() => {
|
|
|
74
77
|
const { viewed, rawed, canUnchangeVisible } = funcs
|
|
75
78
|
const onViewedChange = function (evt) {
|
|
76
79
|
const checked = (evt.target as HTMLInputElement).checked
|
|
77
|
-
|
|
80
|
+
funcs.updateViewed(checked)
|
|
78
81
|
}
|
|
79
82
|
const onRawedChange = function (evt) {
|
|
80
83
|
const checked = (evt.target as HTMLInputElement).checked
|
|
81
|
-
|
|
84
|
+
funcs.updateRawed(checked)
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
onMounted(() => {
|
|
@@ -87,6 +90,22 @@ onMounted(() => {
|
|
|
87
90
|
|
|
88
91
|
// 变更行数
|
|
89
92
|
const changed = ref({ added: 0, removed: 0 })
|
|
93
|
+
const patchChanged = computed(() => {
|
|
94
|
+
const result = { added: 0, removed: 0 }
|
|
95
|
+
|
|
96
|
+
for (const hunk of parsePatchHunks(props.diffPatch)) {
|
|
97
|
+
for (const line of hunk.lines) {
|
|
98
|
+
if (line.startsWith('+')) {
|
|
99
|
+
result.added++
|
|
100
|
+
} else if (line.startsWith('-')) {
|
|
101
|
+
result.removed++
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return result
|
|
107
|
+
})
|
|
108
|
+
const displayedChanged = computed(() => (props.diffPatch ? patchChanged.value : changed.value))
|
|
90
109
|
function updateChangedLines(newVal) {
|
|
91
110
|
Object.assign(changed.value, newVal)
|
|
92
111
|
}
|
package/src/DiffViewer/const.ts
CHANGED
|
@@ -1 +1,11 @@
|
|
|
1
|
+
/** 顶部工具栏的固定高度,单位:像素。 */
|
|
1
2
|
export const HEIGHT_TOP_BAR = 32
|
|
3
|
+
|
|
4
|
+
/** Diff 编辑器中单行代码的默认高度,单位:像素。 */
|
|
5
|
+
export const HEIGHT_CODE_LINE = 18
|
|
6
|
+
|
|
7
|
+
/** 未变更代码折叠区域占用的固定高度,单位:像素。 */
|
|
8
|
+
export const HEIGHT_HIDDEN_REGION = 24
|
|
9
|
+
|
|
10
|
+
/** Diff 编辑器横向滚动条的默认高度,单位:像素。 */
|
|
11
|
+
export const HEIGHT_HORIZONTAL_SCROLLBAR = 8
|
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
import { diffLines } from 'diff'
|
|
2
2
|
import type { WtoolDiffViewerProps } from '@/types'
|
|
3
|
-
import {
|
|
3
|
+
import { HEIGHT_CODE_LINE, HEIGHT_HIDDEN_REGION, HEIGHT_HORIZONTAL_SCROLLBAR } from '../const'
|
|
4
|
+
import { patch2PairWithLayout } from './patch2Pair'
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
interface HeightCache {
|
|
7
|
+
patch?: string
|
|
8
|
+
origContent?: string | null
|
|
9
|
+
modContent?: string | null
|
|
10
|
+
minPx: number
|
|
11
|
+
maxPx: number
|
|
12
|
+
unchangedCtxLineNum: number
|
|
13
|
+
unchangedMinLineNum: number
|
|
14
|
+
heights: Partial<Record<'visible' | 'hidden', number>>
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const heightMap = new Map<string, HeightCache>()
|
|
8
18
|
|
|
9
19
|
interface CommonParams {
|
|
10
20
|
minPx: number
|
|
11
21
|
maxPx: number
|
|
12
22
|
unchangedVisiable: boolean
|
|
13
23
|
unchangedCtxLineNum: number
|
|
24
|
+
unchangedMinLineNum: number
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
interface ChangedLineBlock {
|
|
@@ -66,15 +77,17 @@ function getChangedLineInfo(origContent: string, modContent: string): ChangedLin
|
|
|
66
77
|
* 区间必须按 start 升序喂入,每次 feed 后可读取 maxReached 判断是否达到像素上限。
|
|
67
78
|
* @param totalLines 文件总行数,用于 clamp context 窗口上界及判断尾部是否有折叠 widget
|
|
68
79
|
* @param ctx 每个变更块上下保留的 context 行数,对应 Monaco contextLineCount
|
|
80
|
+
* @param minHidden Monaco minimumLineCount,小于此值的未变更区不折叠
|
|
69
81
|
* @param maxPx 像素上限,达到后 maxReached 为 true,调用方应立即停止 feed
|
|
70
82
|
*/
|
|
71
|
-
function makeVisibleLineCounter(totalLines: number, ctx: number, maxPx: number) {
|
|
83
|
+
function makeVisibleLineCounter(totalLines: number, ctx: number, minHidden: number, maxPx: number) {
|
|
72
84
|
let visible = 0 // 已 commit 的确定可见行数(不含当前 pending 块)
|
|
73
|
-
let gaps = 0 // gap widget
|
|
85
|
+
let gaps = 0 // gap widget 数量(每个折叠区域占固定高度,非代码行高)
|
|
74
86
|
let pendingStart = -1 // 当前待合并窗口的起始行(-1 表示无 pending)
|
|
75
87
|
let pendingEnd = -1 // 当前待合并窗口的结束行
|
|
76
88
|
|
|
77
|
-
const usedPx = () =>
|
|
89
|
+
const usedPx = () =>
|
|
90
|
+
visible * HEIGHT_CODE_LINE + gaps * HEIGHT_HIDDEN_REGION + HEIGHT_HORIZONTAL_SCROLLBAR
|
|
78
91
|
|
|
79
92
|
const commitPending = () => {
|
|
80
93
|
if (pendingStart === -1) return
|
|
@@ -86,9 +99,17 @@ function makeVisibleLineCounter(totalLines: number, ctx: number, maxPx: number)
|
|
|
86
99
|
|
|
87
100
|
return {
|
|
88
101
|
feed(s: number, e: number) {
|
|
89
|
-
|
|
102
|
+
let winStart = Math.max(1, s - ctx)
|
|
90
103
|
const winEnd = Math.min(totalLines, e + ctx)
|
|
91
|
-
if (pendingEnd === -1
|
|
104
|
+
if (pendingEnd === -1) {
|
|
105
|
+
if (winStart - 1 < minHidden) winStart = 1
|
|
106
|
+
pendingStart = winStart
|
|
107
|
+
pendingEnd = winEnd
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const hiddenLineCount = winStart - pendingEnd - 1
|
|
112
|
+
if (hiddenLineCount > 0 && hiddenLineCount >= minHidden) {
|
|
92
113
|
commitPending() // 新窗口与 pending 有间隙:先提交 pending,再开新窗口
|
|
93
114
|
pendingStart = winStart
|
|
94
115
|
pendingEnd = winEnd
|
|
@@ -99,7 +120,14 @@ function makeVisibleLineCounter(totalLines: number, ctx: number, maxPx: number)
|
|
|
99
120
|
flush() {
|
|
100
121
|
const lastEnd = pendingEnd
|
|
101
122
|
commitPending()
|
|
102
|
-
|
|
123
|
+
const trailingLineCount = totalLines - lastEnd
|
|
124
|
+
if (visible > 0 && trailingLineCount > 0) {
|
|
125
|
+
if (trailingLineCount >= minHidden) {
|
|
126
|
+
gaps += 1
|
|
127
|
+
} else {
|
|
128
|
+
visible += trailingLineCount
|
|
129
|
+
}
|
|
130
|
+
}
|
|
103
131
|
return usedPx()
|
|
104
132
|
},
|
|
105
133
|
get usedPx() {
|
|
@@ -119,23 +147,20 @@ const autoHeightPatch = function ({
|
|
|
119
147
|
minPx,
|
|
120
148
|
maxPx,
|
|
121
149
|
unchangedCtxLineNum,
|
|
150
|
+
unchangedMinLineNum,
|
|
122
151
|
}: {
|
|
123
152
|
patch: string
|
|
124
153
|
} & CommonParams): number {
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
154
|
+
const { changedLineBlocks, totalLines } = patch2PairWithLayout(patch)
|
|
155
|
+
if (changedLineBlocks.length === 0) return minPx
|
|
127
156
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (const h of hunks) {
|
|
133
|
-
const hunkEnd = Math.max(h.origStart + h.origCount - 1, h.modStart + h.modCount - 1)
|
|
134
|
-
counter.feed(h.origStart, hunkEnd)
|
|
157
|
+
const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, unchangedMinLineNum, maxPx)
|
|
158
|
+
for (const block of changedLineBlocks) {
|
|
159
|
+
counter.feed(block.start, block.end)
|
|
135
160
|
if (counter.maxReached) return maxPx
|
|
136
161
|
}
|
|
137
162
|
|
|
138
|
-
return Math.max(minPx, counter.flush())
|
|
163
|
+
return Math.min(maxPx, Math.max(minPx, counter.flush()))
|
|
139
164
|
}
|
|
140
165
|
|
|
141
166
|
const autoHeightPair = function ({
|
|
@@ -144,6 +169,7 @@ const autoHeightPair = function ({
|
|
|
144
169
|
maxPx,
|
|
145
170
|
unchangedVisiable,
|
|
146
171
|
unchangedCtxLineNum,
|
|
172
|
+
unchangedMinLineNum,
|
|
147
173
|
}: {
|
|
148
174
|
pair: WtoolDiffViewerProps['diffPair']
|
|
149
175
|
} & CommonParams): number {
|
|
@@ -156,13 +182,14 @@ const autoHeightPair = function ({
|
|
|
156
182
|
const totalLines = Math.max(origLines.length, modLines.length)
|
|
157
183
|
|
|
158
184
|
if (unchangedVisiable) {
|
|
159
|
-
|
|
185
|
+
const contentHeight = totalLines * HEIGHT_CODE_LINE + HEIGHT_HORIZONTAL_SCROLLBAR
|
|
186
|
+
return Math.max(minPx, Math.min(contentHeight, maxPx))
|
|
160
187
|
}
|
|
161
188
|
|
|
162
189
|
const { blocks, totalLines: diffTotalLines } = getChangedLineInfo(origContent, modContent)
|
|
163
190
|
if (blocks.length === 0) return minPx
|
|
164
191
|
|
|
165
|
-
const counter = makeVisibleLineCounter(diffTotalLines, unchangedCtxLineNum, maxPx)
|
|
192
|
+
const counter = makeVisibleLineCounter(diffTotalLines, unchangedCtxLineNum, unchangedMinLineNum, maxPx)
|
|
166
193
|
for (const block of blocks) {
|
|
167
194
|
counter.feed(block.start, block.end)
|
|
168
195
|
if (counter.maxReached) return maxPx
|
|
@@ -175,7 +202,7 @@ const autoHeightPair = function ({
|
|
|
175
202
|
return result
|
|
176
203
|
}
|
|
177
204
|
|
|
178
|
-
const height2Num = (heightStr: string): number => {
|
|
205
|
+
export const height2Num = (heightStr: string): number => {
|
|
179
206
|
if (heightStr.endsWith('vh')) {
|
|
180
207
|
const vh = parseFloat(heightStr)
|
|
181
208
|
return Math.round((vh / 100) * document.documentElement.clientHeight)
|
|
@@ -191,6 +218,7 @@ export const autoHeight = function ({
|
|
|
191
218
|
minHeight,
|
|
192
219
|
unchangedVisiable,
|
|
193
220
|
unchangedCtxLineNum,
|
|
221
|
+
unchangedMinLineNum = 1,
|
|
194
222
|
}: {
|
|
195
223
|
id: string
|
|
196
224
|
patch?: string
|
|
@@ -199,19 +227,45 @@ export const autoHeight = function ({
|
|
|
199
227
|
minHeight: string
|
|
200
228
|
unchangedVisiable: boolean
|
|
201
229
|
unchangedCtxLineNum: number
|
|
230
|
+
unchangedMinLineNum?: number
|
|
202
231
|
}): number {
|
|
203
232
|
const cacheKey = unchangedVisiable ? 'visible' : 'hidden'
|
|
204
|
-
const
|
|
205
|
-
const
|
|
233
|
+
const [minPx, maxPx] = [minHeight, maxHeight].map(height2Num)
|
|
234
|
+
const origContent = pair?.[0]?.content
|
|
235
|
+
const modContent = pair?.[1]?.content
|
|
236
|
+
const cache = heightMap.get(id)
|
|
237
|
+
const cacheMatched =
|
|
238
|
+
cache?.patch === patch &&
|
|
239
|
+
cache?.origContent === origContent &&
|
|
240
|
+
cache?.modContent === modContent &&
|
|
241
|
+
cache?.minPx === minPx &&
|
|
242
|
+
cache?.maxPx === maxPx &&
|
|
243
|
+
cache?.unchangedCtxLineNum === unchangedCtxLineNum &&
|
|
244
|
+
cache?.unchangedMinLineNum === unchangedMinLineNum
|
|
245
|
+
const cachedHeight = cacheMatched ? cache.heights[cacheKey] : undefined
|
|
206
246
|
if (cachedHeight !== undefined) return cachedHeight
|
|
207
247
|
|
|
208
|
-
const
|
|
209
|
-
|
|
248
|
+
const commonParams: CommonParams = {
|
|
249
|
+
minPx,
|
|
250
|
+
maxPx,
|
|
251
|
+
unchangedVisiable,
|
|
252
|
+
unchangedCtxLineNum,
|
|
253
|
+
unchangedMinLineNum,
|
|
254
|
+
}
|
|
210
255
|
const height = patch ? autoHeightPatch({ patch, ...commonParams }) : autoHeightPair({ pair, ...commonParams })
|
|
211
256
|
|
|
212
257
|
heightMap.set(id, {
|
|
213
|
-
|
|
214
|
-
|
|
258
|
+
patch,
|
|
259
|
+
origContent,
|
|
260
|
+
modContent,
|
|
261
|
+
minPx,
|
|
262
|
+
maxPx,
|
|
263
|
+
unchangedCtxLineNum,
|
|
264
|
+
unchangedMinLineNum,
|
|
265
|
+
heights: {
|
|
266
|
+
...(cacheMatched ? cache.heights : {}),
|
|
267
|
+
[cacheKey]: height,
|
|
268
|
+
},
|
|
215
269
|
})
|
|
216
270
|
|
|
217
271
|
return height
|
|
@@ -5,6 +5,17 @@ export interface FilePair {
|
|
|
5
5
|
content: string
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
+
export interface PatchChangedLineBlock {
|
|
9
|
+
start: number
|
|
10
|
+
end: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PatchPairLayout {
|
|
14
|
+
pair: FilePair[]
|
|
15
|
+
changedLineBlocks: PatchChangedLineBlock[]
|
|
16
|
+
totalLines: number
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
/**
|
|
9
20
|
* 将 unified diff patch 转换为 [original, modified] 文件对。
|
|
10
21
|
*
|
|
@@ -15,12 +26,16 @@ export interface FilePair {
|
|
|
15
26
|
* - 连续的删除('-')/ 新增('+')块:收集后成对对齐写入,
|
|
16
27
|
* 行数较少的一侧补空行,使 Monaco 能在同一视觉行渲染替换关系
|
|
17
28
|
*/
|
|
18
|
-
export const
|
|
29
|
+
export const patch2PairWithLayout = function (patch: string): PatchPairLayout {
|
|
19
30
|
if (!patch) {
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
31
|
+
return {
|
|
32
|
+
pair: [
|
|
33
|
+
{ filename: '', content: '' },
|
|
34
|
+
{ filename: '', content: '' },
|
|
35
|
+
],
|
|
36
|
+
changedLineBlocks: [],
|
|
37
|
+
totalLines: 1,
|
|
38
|
+
}
|
|
24
39
|
}
|
|
25
40
|
|
|
26
41
|
const { origFilename, modFilename } = parsePatchFilenames(patch)
|
|
@@ -28,12 +43,17 @@ export const patch2Pair = function (patch: string): FilePair[] {
|
|
|
28
43
|
|
|
29
44
|
const origLines: string[] = []
|
|
30
45
|
const modLines: string[] = []
|
|
46
|
+
const changedLineBlocks: PatchChangedLineBlock[] = []
|
|
31
47
|
|
|
32
48
|
const pendingDel: string[] = []
|
|
33
49
|
const pendingAdd: string[] = []
|
|
34
50
|
|
|
35
51
|
const flushPending = () => {
|
|
36
52
|
const maxLen = Math.max(pendingDel.length, pendingAdd.length)
|
|
53
|
+
if (maxLen > 0) {
|
|
54
|
+
const start = Math.max(origLines.length, modLines.length) + 1
|
|
55
|
+
changedLineBlocks.push({ start, end: start + maxLen - 1 })
|
|
56
|
+
}
|
|
37
57
|
for (let i = 0; i < maxLen; i++) {
|
|
38
58
|
origLines.push(pendingDel[i] ?? '')
|
|
39
59
|
modLines.push(pendingAdd[i] ?? '')
|
|
@@ -78,8 +98,16 @@ export const patch2Pair = function (patch: string): FilePair[] {
|
|
|
78
98
|
flushPending()
|
|
79
99
|
}
|
|
80
100
|
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
101
|
+
return {
|
|
102
|
+
pair: [
|
|
103
|
+
{ filename: origFilename, content: origLines.join('\n') },
|
|
104
|
+
{ filename: modFilename, content: modLines.join('\n') },
|
|
105
|
+
],
|
|
106
|
+
changedLineBlocks,
|
|
107
|
+
totalLines: Math.max(origLines.length, modLines.length),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const patch2Pair = function (patch: string): FilePair[] {
|
|
112
|
+
return patch2PairWithLayout(patch).pair
|
|
85
113
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M13 3L13.7071 2.29289C13.5196 2.10536 13.2652 2 13 2V3ZM19 9H20C20 8.73478 19.8946 8.48043 19.7071 8.29289L19 9ZM13.109 8.45399L14 8V8L13.109 8.45399ZM13.546 8.89101L14 8L13.546 8.89101ZM10 13C10 12.4477 9.55228 12 9 12C8.44772 12 8 12.4477 8 13H10ZM8 16C8 16.5523 8.44772 17 9 17C9.55228 17 10 16.5523 10 16H8ZM8.5 9C7.94772 9 7.5 9.44772 7.5 10C7.5 10.5523 7.94772 11 8.5 11V9ZM9.5 11C10.0523 11 10.5 10.5523 10.5 10C10.5 9.44772 10.0523 9 9.5 9V11ZM8.5 6C7.94772 6 7.5 6.44772 7.5 7C7.5 7.55228 7.94772 8 8.5 8V6ZM9.5 8C10.0523 8 10.5 7.55228 10.5 7C10.5 6.44772 10.0523 6 9.5 6V8ZM17.908 20.782L17.454 19.891L17.454 19.891L17.908 20.782ZM18.782 19.908L19.673 20.362L18.782 19.908ZM5.21799 19.908L4.32698 20.362H4.32698L5.21799 19.908ZM6.09202 20.782L6.54601 19.891L6.54601 19.891L6.09202 20.782ZM6.09202 3.21799L5.63803 2.32698L5.63803 2.32698L6.09202 3.21799ZM5.21799 4.09202L4.32698 3.63803L4.32698 3.63803L5.21799 4.09202ZM12 3V7.4H14V3H12ZM14.6 10H19V8H14.6V10ZM12 7.4C12 7.66353 11.9992 7.92131 12.0169 8.13823C12.0356 8.36682 12.0797 8.63656 12.218 8.90798L14 8C14.0293 8.05751 14.0189 8.08028 14.0103 7.97537C14.0008 7.85878 14 7.69653 14 7.4H12ZM14.6 8C14.3035 8 14.1412 7.99922 14.0246 7.9897C13.9197 7.98113 13.9425 7.9707 14 8L13.092 9.78201C13.3634 9.92031 13.6332 9.96438 13.8618 9.98305C14.0787 10.0008 14.3365 10 14.6 10V8ZM12.218 8.90798C12.4097 9.2843 12.7157 9.59027 13.092 9.78201L14 8V8L12.218 8.90798ZM8 13V16H10V13H8ZM8.5 11H9.5V9H8.5V11ZM8.5 8H9.5V6H8.5V8ZM13 2H8.2V4H13V2ZM4 6.2V17.8H6V6.2H4ZM8.2 22H15.8V20H8.2V22ZM20 17.8V9H18V17.8H20ZM19.7071 8.29289L13.7071 2.29289L12.2929 3.70711L18.2929 9.70711L19.7071 8.29289ZM15.8 22C16.3436 22 16.8114 22.0008 17.195 21.9694C17.5904 21.9371 17.9836 21.8658 18.362 21.673L17.454 19.891C17.4045 19.9162 17.3038 19.9539 17.0322 19.9761C16.7488 19.9992 16.3766 20 15.8 20V22ZM18 17.8C18 18.3766 17.9992 18.7488 17.9761 19.0322C17.9539 19.3038 17.9162 19.4045 17.891 19.454L19.673 20.362C19.8658 19.9836 19.9371 19.5904 19.9694 19.195C20.0008 18.8114 20 18.3436 20 17.8H18ZM18.362 21.673C18.9265 21.3854 19.3854 20.9265 19.673 20.362L17.891 19.454C17.7951 19.6422 17.6422 19.7951 17.454 19.891L18.362 21.673ZM4 17.8C4 18.3436 3.99922 18.8114 4.03057 19.195C4.06287 19.5904 4.13419 19.9836 4.32698 20.362L6.10899 19.454C6.0838 19.4045 6.04612 19.3038 6.02393 19.0322C6.00078 18.7488 6 18.3766 6 17.8H4ZM8.2 20C7.62345 20 7.25117 19.9992 6.96784 19.9761C6.69617 19.9539 6.59545 19.9162 6.54601 19.891L5.63803 21.673C6.01641 21.8658 6.40963 21.9371 6.80497 21.9694C7.18864 22.0008 7.65645 22 8.2 22V20ZM4.32698 20.362C4.6146 20.9265 5.07354 21.3854 5.63803 21.673L6.54601 19.891C6.35785 19.7951 6.20487 19.6422 6.10899 19.454L4.32698 20.362ZM8.2 2C7.65645 2 7.18864 1.99922 6.80497 2.03057C6.40963 2.06287 6.01641 2.13419 5.63803 2.32698L6.54601 4.10899C6.59545 4.0838 6.69617 4.04612 6.96784 4.02393C7.25117 4.00078 7.62345 4 8.2 4V2ZM6 6.2C6 5.62345 6.00078 5.25117 6.02393 4.96784C6.04612 4.69617 6.0838 4.59545 6.10899 4.54601L4.32698 3.63803C4.13419 4.01641 4.06287 4.40963 4.03057 4.80497C3.99922 5.18864 4 5.65645 4 6.2H6ZM5.63803 2.32698C5.07354 2.6146 4.6146 3.07354 4.32698 3.63803L6.10899 4.54601C6.20487 4.35785 6.35785 4.20487 6.54601 4.10899L5.63803 2.32698Z" fill="#000000"/>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
2
|
+
<svg width="800px" height="800px" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
3
|
+
<path d="M0 1H6L9 4H16V14H0V1Z" fill="#54aeff"/>
|
|
4
|
+
</svg>
|
package/src/index.ts
CHANGED