@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.
Files changed (39) hide show
  1. package/README.md +140 -41
  2. package/dist/DiffFiles/DiffFiles.vue.d.ts +12 -0
  3. package/dist/DiffFiles/DiffList/DiffList.vue.d.ts +94 -0
  4. package/dist/DiffFiles/FileExplore/FileExplore.vue.d.ts +510 -0
  5. package/dist/DiffFiles/FileExplore/FileSearch.vue.d.ts +11 -0
  6. package/dist/DiffFiles/FileExplore/fileTree.d.ts +17 -0
  7. package/dist/DiffFiles/createDiffFiles.d.ts +13 -0
  8. package/dist/DiffFiles/index.d.ts +3 -0
  9. package/dist/DiffFiles/types.d.ts +12 -0
  10. package/dist/DiffFiles/useDiffFiles.d.ts +23 -0
  11. package/dist/DiffViewer/DiffViewer.vue.d.ts +15 -4
  12. package/dist/DiffViewer/TopBar.vue.d.ts +2 -0
  13. package/dist/DiffViewer/const.d.ts +7 -0
  14. package/dist/DiffViewer/utils/autoHeight.d.ts +3 -1
  15. package/dist/DiffViewer/utils/patch2Pair.d.ts +10 -0
  16. package/dist/index.d.ts +1 -0
  17. package/dist/types.d.ts +15 -5
  18. package/dist/wtool-vdiff.cjs.js +42 -39
  19. package/dist/wtool-vdiff.es.js +9306 -5945
  20. package/package.json +6 -7
  21. package/src/DiffFiles/DiffFiles.vue +97 -18
  22. package/src/DiffFiles/DiffList/DiffList.vue +125 -0
  23. package/src/DiffFiles/FileExplore/FileExplore.vue +165 -7
  24. package/src/DiffFiles/FileExplore/FileSearch.vue +81 -0
  25. package/src/DiffFiles/FileExplore/fileTree.ts +102 -0
  26. package/src/DiffFiles/createDiffFiles.ts +70 -0
  27. package/src/DiffFiles/index.ts +11 -0
  28. package/src/DiffFiles/types.ts +15 -0
  29. package/src/DiffFiles/useDiffFiles.ts +56 -0
  30. package/src/DiffViewer/DiffViewer.vue +22 -8
  31. package/src/DiffViewer/MonacoDiffViewer.vue +21 -2
  32. package/src/DiffViewer/TopBar.vue +23 -4
  33. package/src/DiffViewer/const.ts +10 -0
  34. package/src/DiffViewer/utils/autoHeight.ts +83 -29
  35. package/src/DiffViewer/utils/patch2Pair.ts +37 -9
  36. package/src/assets/file.svg +4 -0
  37. package/src/assets/folder.svg +4 -0
  38. package/src/index.ts +1 -0
  39. 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
- // 当前高度为代码高度,需要加上 topBar 高度才是完整高度
121
- return `${height + HEIGHT_TOP_BAR}px`
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: 8,
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'">+{{ changed.added }}</div>
8
- <div class="del" v-if="diffType !== 'add'">-{{ changed.removed }}</div>
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
- viewed.value = checked
80
+ funcs.updateViewed(checked)
78
81
  }
79
82
  const onRawedChange = function (evt) {
80
83
  const checked = (evt.target as HTMLInputElement).checked
81
- rawed.value = checked
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
  }
@@ -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 { parsePatchHunks } from '@/utils/patch'
3
+ import { HEIGHT_CODE_LINE, HEIGHT_HIDDEN_REGION, HEIGHT_HORIZONTAL_SCROLLBAR } from '../const'
4
+ import { patch2PairWithLayout } from './patch2Pair'
4
5
 
5
- const LINE_HEIGHT = 18
6
- const GAP_HEIGHT = 24
7
- const heightMap = new Map<string, Partial<Record<'visible' | 'hidden', number>>>()
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 数量(每个折叠区域占 GAP_HEIGHT,非 LINE_HEIGHT)
85
+ let gaps = 0 // gap widget 数量(每个折叠区域占固定高度,非代码行高)
74
86
  let pendingStart = -1 // 当前待合并窗口的起始行(-1 表示无 pending)
75
87
  let pendingEnd = -1 // 当前待合并窗口的结束行
76
88
 
77
- const usedPx = () => visible * LINE_HEIGHT + gaps * GAP_HEIGHT
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
- const winStart = Math.max(1, s - ctx)
102
+ let winStart = Math.max(1, s - ctx)
90
103
  const winEnd = Math.min(totalLines, e + ctx)
91
- if (pendingEnd === -1 || winStart > 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
- if (visible > 0 && lastEnd !== totalLines) gaps += 1
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 hunks = parsePatchHunks(patch)
126
- if (hunks.length === 0) return minPx
154
+ const { changedLineBlocks, totalLines } = patch2PairWithLayout(patch)
155
+ if (changedLineBlocks.length === 0) return minPx
127
156
 
128
- const lastHunk = hunks[hunks.length - 1]
129
- const totalLines = Math.max(lastHunk.origStart + lastHunk.origCount - 1, lastHunk.modStart + lastHunk.modCount - 1)
130
- const counter = makeVisibleLineCounter(totalLines, unchangedCtxLineNum, maxPx)
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
- return Math.max(minPx, Math.min(totalLines * LINE_HEIGHT, maxPx))
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 cache = heightMap.get(id) || {}
205
- const cachedHeight = cache?.[cacheKey]
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 [minPx, maxPx] = [minHeight, maxHeight].map(height2Num)
209
- const commonParams: CommonParams = { minPx, maxPx, unchangedVisiable, unchangedCtxLineNum }
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
- ...cache,
214
- [cacheKey]: height,
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 patch2Pair = function (patch: string): FilePair[] {
29
+ export const patch2PairWithLayout = function (patch: string): PatchPairLayout {
19
30
  if (!patch) {
20
- return [
21
- { filename: '', content: '' },
22
- { filename: '', content: '' },
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
- { filename: origFilename, content: origLines.join('\n') },
83
- { filename: modFilename, content: modLines.join('\n') },
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
@@ -1,2 +1,3 @@
1
1
  export * from './DiffViewer'
2
+ export * from './DiffFiles'
2
3
  export { default as loader } from '@monaco-editor/loader'