@yuhufe/wtool-vdiff 0.0.1

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/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@yuhufe/wtool-vdiff",
3
+ "private": false,
4
+ "version": "0.0.1",
5
+ "type": "module",
6
+ "description": "Monaco diff viewer as Vue 3 Web Component",
7
+ "keywords": [
8
+ "git",
9
+ "diff",
10
+ "monaco",
11
+ "vue3",
12
+ "web-component"
13
+ ],
14
+ "scripts": {
15
+ "dev": "pnpm run dev:site",
16
+ "dev:lib": "vite build --watch --mode development",
17
+ "build:site": "vite build --mode production --config site/vite.config.ts",
18
+ "dev:site": "vite --config site/vite.config.ts",
19
+ "build": "vite build --mode production"
20
+ },
21
+ "main": "./dist/wtool-vdiff.cjs.js",
22
+ "module": "./dist/wtool-vdiff.es.js",
23
+ "files": [
24
+ "dist/*",
25
+ "src/*"
26
+ ],
27
+ "types": "dist/wtool-vdiff.es.d.ts",
28
+ "publishConfig": {
29
+ "access": "public"
30
+ },
31
+ "author": "",
32
+ "license": "ISC",
33
+ "peerDependencies": {
34
+ "vue": "^3.5.0"
35
+ },
36
+ "dependencies": {
37
+ "@yuhufe/web-ui": "workspace:*",
38
+ "@monaco-editor/loader": "^1.6.1",
39
+ "monaco-editor": "^0.53.0",
40
+ "less": "4.6.4"
41
+ },
42
+ "devDependencies": {
43
+ "@vitejs/plugin-vue": "^5.2.0",
44
+ "typescript": "^5.9.3",
45
+ "vite": "^7.3.1",
46
+ "vite-plugin-dts": "^4.5.3",
47
+ "vue": "^3.5.0"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/defghy/web-toolkits.git"
52
+ }
53
+ }
@@ -0,0 +1,143 @@
1
+ <template>
2
+ <div class="diff-viewer-wrap" :style="viewerStyle">
3
+ <TopBar class="top-bar" :diffPair="diffPair" />
4
+ <div class="content-wrap" v-show="!viewed" v-loading="loading">
5
+ <MonacoDiffViewer
6
+ class="monaco-container"
7
+ :originalCode="originalCode"
8
+ :modifiedCode="modifiedCode"
9
+ :language="language"
10
+ :options="mergedOptions"
11
+ :modelOptions="modelOptions"
12
+ @render-complete="onMonacoRenderComplete"
13
+ />
14
+ </div>
15
+ </div>
16
+ </template>
17
+
18
+ <script setup lang="ts">
19
+ import { ref, computed } from 'vue'
20
+
21
+ import { loadingDirective } from '@yuhufe/web-ui'
22
+ import type { DiffEditorOptions, WtoolDiffViewerProps, ModelOptions } from '../types'
23
+ import MonacoDiffViewer from './MonacoDiffViewer.vue'
24
+ import TopBar from './TopBar.vue'
25
+ import { useDiffViewer } from './useDiffView'
26
+ import { patch2Pair } from './utils/patch2Pair'
27
+ import { autoHeight } from './utils/autoHeight'
28
+
29
+ const vLoading = loadingDirective
30
+ const loading = ref(true)
31
+
32
+ const _renderStart = performance.now()
33
+ const onMonacoRenderComplete = () => {
34
+ const cost = performance.now() - _renderStart
35
+ console.log(`[DiffViewer] 渲染耗时: ${cost.toFixed(2)} ms`)
36
+ loading.value = false
37
+ }
38
+
39
+ const props = withDefaults(defineProps<WtoolDiffViewerProps>(), {
40
+ diffPair: () => [],
41
+ diffPatch: '',
42
+ language: 'plaintext',
43
+ options: () => ({}),
44
+ modelOptions: () => ({}),
45
+ })
46
+
47
+ const diffPair = ref(props.diffPair || null)
48
+ const initDiff = function () {
49
+ if (diffPair.value?.length) {
50
+ return
51
+ }
52
+
53
+ // diffPatch => diffPair
54
+ diffPair.value = patch2Pair(props.diffPatch)
55
+ }
56
+ initDiff()
57
+ console.log(`[DiffViewer] patch耗时: ${(performance.now() - _renderStart).toFixed(2)} ms`)
58
+
59
+ const originalCode = computed(() => diffPair.value[0].content)
60
+ const modifiedCode = computed(() => diffPair.value[1].content)
61
+
62
+ const { funcs, registerFunc } = useDiffViewer({ isMaster: true })
63
+
64
+ /** raw 勾选时展示全文;未勾选时折叠无变更块,仅保留差异附近的上下文行 */
65
+ const mergedOptions = computed(() => {
66
+ // canUnchangeVisible=false(patch 模式)时,未改动区域为空行,强制折叠且忽略 rawed
67
+ const forceHide = !canUnchangeVisible.value
68
+
69
+ // 折叠上下文
70
+ let hideUnchangedRegions = props.options.hideUnchangedRegions || {}
71
+ if (!forceHide && rawed.value) {
72
+ hideUnchangedRegions = {
73
+ enabled: false,
74
+ ...hideUnchangedRegions,
75
+ }
76
+ } else {
77
+ hideUnchangedRegions = {
78
+ enabled: true,
79
+ contextLineCount: 3,
80
+ ...hideUnchangedRegions,
81
+ }
82
+ }
83
+
84
+ return {
85
+ ...props.options,
86
+ hideUnchangedRegions,
87
+ }
88
+ })
89
+
90
+ const viewed = ref<boolean>(false) // 是否已读
91
+ const rawed = ref<boolean>(false) // 是否显示原始文件
92
+ const canUnchangeVisible = ref(!props.diffPatch) // patch 模式下未改动区域为空行,不可展示
93
+
94
+ // 编辑器样式
95
+ const viewerHeight = computed(() => {
96
+ if (props.viewerStyle?.height) {
97
+ return props.viewerStyle.height
98
+ }
99
+
100
+ const heightRange = {
101
+ minHeight: '100px',
102
+ maxHeight: '250px',
103
+ ...(props.viewerStyle || {}),
104
+ }
105
+
106
+ const height = autoHeight({
107
+ patch: props.diffPatch,
108
+ pair: props.diffPair,
109
+ ...heightRange,
110
+ unchangedVisiable: funcs.rawed.value,
111
+ unchangedCtxLineNum: mergedOptions.value.hideUnchangedRegions.contextLineCount!,
112
+ })
113
+
114
+ return `${height}px`
115
+ })
116
+ const viewerStyle = computed(() => {
117
+ return {
118
+ '--viewer-width': props.viewerStyle?.width || '100%',
119
+ '--viewer-height': viewerHeight.value,
120
+ }
121
+ })
122
+
123
+ registerFunc({
124
+ viewed,
125
+ rawed,
126
+ canUnchangeVisible,
127
+ })
128
+ </script>
129
+
130
+ <style scoped>
131
+ .diff-viewer-wrap {
132
+ width: var(--viewer-width);
133
+ border: 1px solid #ddd;
134
+
135
+ .top-bar {
136
+ flex-shrink: 0;
137
+ }
138
+ .content-wrap {
139
+ height: var(--viewer-height);
140
+ overflow: hidden;
141
+ }
142
+ }
143
+ </style>
@@ -0,0 +1,178 @@
1
+ <template>
2
+ <div ref="containerEl" class="monaco-editor-container" :class="{ 'hide-unchanged-actions': !canUnchangeVisible }" />
3
+ </template>
4
+
5
+ <script setup lang="ts">
6
+ import { ref, watch, onMounted, onBeforeUnmount, shallowRef } from 'vue'
7
+ import loader from '@monaco-editor/loader'
8
+ import type * as Monaco from 'monaco-editor'
9
+ import { useDiffViewer } from './useDiffView'
10
+
11
+ type DiffEditorOptions = Monaco.editor.IStandaloneDiffEditorConstructionOptions
12
+ type ModelOptions = Monaco.editor.ITextModelUpdateOptions
13
+
14
+ const props = withDefaults(
15
+ defineProps<{
16
+ originalCode?: string
17
+ modifiedCode?: string
18
+ language?: string
19
+ options?: DiffEditorOptions
20
+ modelOptions?: ModelOptions
21
+ }>(),
22
+ {
23
+ originalCode: '',
24
+ modifiedCode: '',
25
+ language: 'plaintext',
26
+ options: () => ({}),
27
+ modelOptions: () => ({}),
28
+ }
29
+ )
30
+
31
+ const emit = defineEmits<{ renderComplete: [] }>()
32
+
33
+ const { funcs } = useDiffViewer()
34
+ const canUnchangeVisible = funcs.canUnchangeVisible
35
+
36
+ const containerEl = ref<HTMLDivElement | null>(null)
37
+ const monacoInstance = shallowRef<typeof Monaco | null>(null)
38
+ const editor = shallowRef<Monaco.editor.IStandaloneDiffEditor | null>(null)
39
+ const originalModel = shallowRef<Monaco.editor.ITextModel | null>(null)
40
+ const modifiedModel = shallowRef<Monaco.editor.ITextModel | null>(null)
41
+
42
+ let disposed = false
43
+
44
+ onMounted(() => {
45
+ disposed = false
46
+
47
+ const init = async () => {
48
+ const monaco = await loader.init()
49
+ if (disposed || !containerEl.value) return
50
+
51
+ monacoInstance.value = monaco
52
+ originalModel.value = monaco.editor.createModel(props.originalCode, props.language)
53
+ modifiedModel.value = monaco.editor.createModel(props.modifiedCode, props.language)
54
+
55
+ editor.value = monaco.editor.createDiffEditor(containerEl.value, {
56
+ automaticLayout: true,
57
+ readOnly: true,
58
+ renderSideBySide: true,
59
+ useInlineViewWhenSpaceIsLimited: false,
60
+ scrollBeyondLastLine: false,
61
+ hideUnchangedRegions: {
62
+ enabled: true,
63
+ contextLineCount: 3,
64
+ },
65
+ scrollbar: {
66
+ verticalScrollbarSize: 8,
67
+ horizontalScrollbarSize: 8,
68
+ },
69
+ ...props.options,
70
+ })
71
+ editor.value.setModel({
72
+ original: originalModel.value,
73
+ modified: modifiedModel.value,
74
+ })
75
+
76
+ if (Object.keys(props.modelOptions).length > 0) {
77
+ originalModel.value.updateOptions(props.modelOptions)
78
+ modifiedModel.value.updateOptions(props.modelOptions)
79
+ }
80
+
81
+ // 仅在首次 diff 计算完成后统计行数,之后立即注销监听。
82
+ // Monaco 用 endLineNumber === 0 表示该侧无变更行(纯删除或纯新增),span 需特殊处理。
83
+ const onDidUpdateDiffDisposable = editor.value.onDidUpdateDiff(() => {
84
+ const changes = editor.value?.getLineChanges() ?? []
85
+ const span = (s: number, e: number) => (e === 0 ? 0 : e - s + 1)
86
+ const added = changes.reduce((n, c) => n + span(c.modifiedStartLineNumber, c.modifiedEndLineNumber), 0)
87
+ const removed = changes.reduce((n, c) => n + span(c.originalStartLineNumber, c.originalEndLineNumber), 0)
88
+ funcs.updateChangedLines?.({ added, removed })
89
+ emit('renderComplete')
90
+ onDidUpdateDiffDisposable.dispose()
91
+ })
92
+ }
93
+
94
+ void init()
95
+ })
96
+
97
+ onBeforeUnmount(() => {
98
+ disposed = true
99
+ editor.value?.dispose()
100
+ originalModel.value?.dispose()
101
+ modifiedModel.value?.dispose()
102
+ editor.value = null
103
+ originalModel.value = null
104
+ modifiedModel.value = null
105
+ monacoInstance.value = null
106
+ })
107
+
108
+ watch(
109
+ () => props.options,
110
+ opts => {
111
+ if (!editor.value) return
112
+ editor.value.updateOptions(opts)
113
+ },
114
+ { deep: true }
115
+ )
116
+
117
+ watch(
118
+ () => props.originalCode,
119
+ v => {
120
+ if (!originalModel.value) return
121
+ if (originalModel.value.getValue() !== v) {
122
+ originalModel.value.setValue(v)
123
+ }
124
+ }
125
+ )
126
+
127
+ watch(
128
+ () => props.modifiedCode,
129
+ v => {
130
+ if (!modifiedModel.value) return
131
+ if (modifiedModel.value.getValue() !== v) {
132
+ modifiedModel.value.setValue(v)
133
+ }
134
+ }
135
+ )
136
+
137
+ watch(
138
+ () => props.language,
139
+ lang => {
140
+ const monaco = monacoInstance.value
141
+ const om = originalModel.value
142
+ const mm = modifiedModel.value
143
+ if (!monaco || !om || !mm) return
144
+ monaco.editor.setModelLanguage(om, lang)
145
+ monaco.editor.setModelLanguage(mm, lang)
146
+ }
147
+ )
148
+
149
+ watch(
150
+ () => props.modelOptions,
151
+ mo => {
152
+ if (!originalModel.value || !modifiedModel.value) return
153
+ originalModel.value.updateOptions(mo)
154
+ modifiedModel.value.updateOptions(mo)
155
+ },
156
+ { deep: true }
157
+ )
158
+ </script>
159
+
160
+ <style scoped>
161
+ .monaco-editor-container {
162
+ width: 100%;
163
+ height: 100%;
164
+ }
165
+ </style>
166
+
167
+ <style>
168
+ /* patch 模式:未改动区域为空行,隐藏 monaco 内置的展开未改动区域按钮(非 scoped,配合 JS class 控制) */
169
+ .monaco-editor-container.hide-unchanged-actions {
170
+ .diff-hidden-lines-widget {
171
+ cursor: not-allowed;
172
+ .diff-hidden-lines {
173
+ pointer-events: none;
174
+ opacity: 0.75;
175
+ }
176
+ }
177
+ }
178
+ </style>
@@ -0,0 +1,121 @@
1
+ <template>
2
+ <div class="top-bar-wrap">
3
+ <div class="title-area">
4
+ <div class="filename">{{ filename }}</div>
5
+ <div class="diff-line-num">
6
+ <div class="add">+{{ changed.added }}</div>
7
+ <div class="del">-{{ changed.removed }}</div>
8
+ </div>
9
+ </div>
10
+ <div class="toolbar">
11
+ <label>
12
+ <input type="checkbox" :checked="viewed" @change="onViewedChange" />
13
+ viewed
14
+ </label>
15
+ <label v-if="canUnchangeVisible">
16
+ <input type="checkbox" :checked="rawed" @change="onRawedChange" />
17
+ raw
18
+ </label>
19
+ </div>
20
+ </div>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import { computed, ref, onMounted, reactive } from 'vue'
25
+ import { useDiffViewer } from './useDiffView'
26
+
27
+ const { funcs, registerFunc } = useDiffViewer()
28
+
29
+ const props = withDefaults(
30
+ defineProps<{
31
+ diffPair?: { filename: string; content: string }[]
32
+ }>(),
33
+ {
34
+ diffPair: () => [],
35
+ }
36
+ )
37
+
38
+ const filename = computed(() => props.diffPair[0].filename)
39
+
40
+ const { viewed, rawed, canUnchangeVisible } = funcs
41
+ const onViewedChange = function (evt) {
42
+ const checked = (evt.target as HTMLInputElement).checked
43
+ viewed.value = checked
44
+ }
45
+ const onRawedChange = function (evt) {
46
+ const checked = (evt.target as HTMLInputElement).checked
47
+ rawed.value = checked
48
+ }
49
+
50
+ onMounted(() => {
51
+ // funcs.options.toolbar?.render($el, {})
52
+ })
53
+
54
+ // 变更行数
55
+ const changed = ref({ added: 0, removed: 0 })
56
+ function updateChangedLines(newVal) {
57
+ Object.assign(changed.value, newVal)
58
+ }
59
+
60
+ registerFunc({
61
+ viewed,
62
+ rawed,
63
+ updateChangedLines,
64
+ })
65
+ </script>
66
+
67
+ <style scoped>
68
+ .top-bar-wrap {
69
+ box-sizing: border-box;
70
+ height: 32px;
71
+
72
+ display: flex;
73
+ overflow: hidden;
74
+ align-items: center;
75
+ background-color: #f7f7f7;
76
+ padding: 4px 8px;
77
+
78
+ .title-area {
79
+ flex: 1;
80
+ display: flex;
81
+ align-items: center;
82
+
83
+ .filename {
84
+ font-size: 14px;
85
+ }
86
+
87
+ .diff-line-num {
88
+ display: inline-flex;
89
+ font-size: 12px;
90
+ font-weight: bold;
91
+ margin-left: 4px;
92
+ .add {
93
+ color: #1a7f37;
94
+ }
95
+ .del {
96
+ color: #d1242f;
97
+ margin-left: 4px;
98
+ }
99
+ }
100
+ }
101
+ .toolbar {
102
+ font-size: 12px;
103
+ flex-shrink: 0;
104
+ display: flex;
105
+ align-items: center;
106
+ gap: 4px;
107
+
108
+ label {
109
+ display: inline-flex;
110
+ align-items: center;
111
+ cursor: pointer;
112
+
113
+ &.disabled {
114
+ opacity: 0.4;
115
+ cursor: not-allowed;
116
+ pointer-events: none;
117
+ }
118
+ }
119
+ }
120
+ }
121
+ </style>
@@ -0,0 +1,49 @@
1
+ import { defineCustomElement } from 'vue'
2
+ import type { WtoolDiffViewerProps } from '../types'
3
+ import DiffViewer from './DiffViewer.vue'
4
+
5
+ export type DiffViewerProps = WtoolDiffViewerProps
6
+
7
+ export const WTOOL_DIFF_VIEWER_TAG = 'wtool-diff-viewer'
8
+
9
+ export const WtoolDiffViewer = defineCustomElement(DiffViewer, {
10
+ shadowRoot: false,
11
+ })
12
+
13
+ export function register(tagName: string = WTOOL_DIFF_VIEWER_TAG): void {
14
+ if (customElements.get(tagName)) return
15
+ customElements.define(tagName, WtoolDiffViewer)
16
+ }
17
+
18
+ export interface DiffViewerInstance {
19
+ update(props: Partial<DiffViewerProps>): void
20
+ destroy(): void
21
+ }
22
+
23
+ function applyProps(el: InstanceType<typeof WtoolDiffViewer>, props: Partial<DiffViewerProps>): void {
24
+ const node = el as unknown as Record<string, unknown>
25
+ for (const key of Object.keys(props) as (keyof DiffViewerProps)[]) {
26
+ const v = props[key]
27
+ if (v !== undefined) {
28
+ node[key as string] = v
29
+ }
30
+ }
31
+ }
32
+
33
+ export function createDiffViewer(target: HTMLElement, initialProps: DiffViewerProps = {}): DiffViewerInstance {
34
+ register()
35
+ const el = new WtoolDiffViewer()
36
+
37
+ applyProps(el, initialProps)
38
+
39
+ target.appendChild(el)
40
+
41
+ return {
42
+ update(newProps: Partial<DiffViewerProps>) {
43
+ applyProps(el, newProps)
44
+ },
45
+ destroy() {
46
+ el.remove()
47
+ },
48
+ }
49
+ }
@@ -0,0 +1,8 @@
1
+ export {
2
+ WTOOL_DIFF_VIEWER_TAG,
3
+ WtoolDiffViewer,
4
+ register as registerDiffViewer,
5
+ createDiffViewer,
6
+ type DiffViewerProps,
7
+ type DiffViewerInstance,
8
+ } from './createDiffViewer'
@@ -0,0 +1,16 @@
1
+ import type { Ref } from 'vue'
2
+ import { useCompExp } from '@yuhufe/web-ui'
3
+
4
+ // 存放单个diffEditor的数据
5
+ export const useDiffViewer = function ({ isMaster = false } = {}) {
6
+ const exp = useCompExp<{
7
+ viewed: Ref<boolean>
8
+ rawed: Ref<boolean>
9
+ updateViewed: (viewed: boolean) => any // 控制是否viewed
10
+ updateRawed: (args: boolean) => any // 控制是否收起展开
11
+ updateChangedLines: (args: { added: number; removed: number }) => any
12
+ canUnchangeVisible: Ref<boolean> // 未改动区域是否可见
13
+ }>({ isMaster, key: 'diffViewer' })
14
+
15
+ return { ...exp }
16
+ }