Skip to content

Commit d36d981

Browse files
feat(devtools): add JSON Diff tool (#722)
* feat(devtools): add json diff navigation and assets * feat(devtools): add json diff state management * feat(devtools): add json diff interface * chore(i18n): add russian json diff locale
1 parent c6b9562 commit d36d981

16 files changed

Lines changed: 1449 additions & 0 deletions

File tree

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
},
5050
"dependencies": {
5151
"@codemirror/commands": "^6.10.3",
52+
"@codemirror/lang-json": "^6.0.2",
5253
"@codemirror/lang-markdown": "^6.5.0",
5354
"@codemirror/language": "^6.12.2",
5455
"@codemirror/language-data": "^6.5.2",
@@ -86,6 +87,7 @@
8687
"i18next-fs-backend": "^2.6.0",
8788
"interactjs": "^1.10.27",
8889
"js-yaml": "^4.1.0",
90+
"jsondiffpatch": "^0.7.3",
8991
"ky": "^1.7.5",
9092
"lucide-vue-next": "^0.476.0",
9193
"markmap-lib": "^0.18.11",

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/main/i18n/locales/en_US/devtools.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@
190190
"description": "Convert text to a URL-friendly slug"
191191
}
192192
},
193+
"compare": {
194+
"label": "Compare",
195+
"jsonDiff": {
196+
"label": "JSON Diff",
197+
"description": "Compare and visualize differences between two JSON documents",
198+
"original": "Original",
199+
"modified": "Modified",
200+
"filters": {
201+
"added": "Added",
202+
"removed": "Removed",
203+
"modified": "Modified"
204+
},
205+
"empty": "Paste JSON into both fields to see the diff",
206+
"errors": {
207+
"invalidJson": "Invalid JSON",
208+
"diffFailed": "Failed to compute diff"
209+
}
210+
}
211+
},
193212
"form": {
194213
"input": "Input",
195214
"output": "Output",

src/main/i18n/locales/ru_RU/devtools.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,25 @@
190190
"description": "Преобразование текста в URL-совместимый slug"
191191
}
192192
},
193+
"compare": {
194+
"label": "Сравнение",
195+
"jsonDiff": {
196+
"label": "JSON Diff",
197+
"description": "Сравнение и визуализация различий между двумя JSON-документами",
198+
"original": "Исходный",
199+
"modified": "Изменённый",
200+
"filters": {
201+
"added": "Добавлено",
202+
"removed": "Удалено",
203+
"modified": "Изменено"
204+
},
205+
"empty": "Вставьте JSON в оба поля, чтобы увидеть различия",
206+
"errors": {
207+
"invalidJson": "Невалидный JSON",
208+
"diffFailed": "Не удалось вычислить diff"
209+
}
210+
}
211+
},
193212
"form": {
194213
"input": "Ввод",
195214
"output": "Вывод",
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
<script setup lang="ts">
2+
import { getJsonDiffFontVariables } from '@/components/devtools/compare/fontVariables'
3+
import { Checkbox } from '@/components/ui/shadcn/checkbox'
4+
import { useEditor, useJsonDiff } from '@/composables'
5+
import { i18n } from '@/electron'
6+
7+
const { settings: editorSettings } = useEditor()
8+
const {
9+
leftText,
10+
rightText,
11+
leftError,
12+
rightError,
13+
viewerError,
14+
filters,
15+
nodes,
16+
isReady,
17+
showEmptyState,
18+
scheduleLeftFormat,
19+
scheduleRightFormat,
20+
formatLeftOnBlur,
21+
formatRightOnBlur,
22+
toggleExpanded,
23+
isExpanded,
24+
} = useJsonDiff()
25+
26+
const title = computed(() => i18n.t('devtools:compare.jsonDiff.label'))
27+
const description = computed(() =>
28+
i18n.t('devtools:compare.jsonDiff.description'),
29+
)
30+
const fontVariables = computed(() => getJsonDiffFontVariables(editorSettings))
31+
const leftTargetScrollTop = ref(0)
32+
const leftTargetScrollLeft = ref(0)
33+
const rightTargetScrollTop = ref(0)
34+
const rightTargetScrollLeft = ref(0)
35+
36+
const filterButtons = computed(() => {
37+
return [
38+
{
39+
key: 'added' as const,
40+
label: i18n.t('devtools:compare.jsonDiff.filters.added'),
41+
},
42+
{
43+
key: 'removed' as const,
44+
label: i18n.t('devtools:compare.jsonDiff.filters.removed'),
45+
},
46+
{
47+
key: 'modified' as const,
48+
label: i18n.t('devtools:compare.jsonDiff.filters.modified'),
49+
},
50+
]
51+
})
52+
53+
function setFilter(
54+
key: 'added' | 'removed' | 'modified',
55+
value: boolean | 'indeterminate',
56+
) {
57+
filters.value[key] = value === true
58+
}
59+
60+
function localizedError(errorKey: string) {
61+
if (!errorKey)
62+
return ''
63+
64+
return i18n.t(`devtools:compare.jsonDiff.errors.${errorKey}`)
65+
}
66+
67+
function syncLeftEditorScroll(position: { top: number, left: number }) {
68+
rightTargetScrollTop.value = position.top
69+
rightTargetScrollLeft.value = position.left
70+
}
71+
72+
function syncRightEditorScroll(position: { top: number, left: number }) {
73+
leftTargetScrollTop.value = position.top
74+
leftTargetScrollLeft.value = position.left
75+
}
76+
</script>
77+
78+
<template>
79+
<div
80+
class="flex h-full min-h-0 flex-col gap-6"
81+
:style="fontVariables"
82+
>
83+
<UiHeading
84+
:title="title"
85+
:description="description"
86+
/>
87+
88+
<div class="flex shrink-0 flex-wrap items-center gap-4">
89+
<label
90+
v-for="button in filterButtons"
91+
:key="button.key"
92+
class="flex cursor-pointer items-center gap-2"
93+
>
94+
<Checkbox
95+
:model-value="filters[button.key]"
96+
@update:model-value="setFilter(button.key, $event)"
97+
/>
98+
<UiText as="span">
99+
{{ button.label }}
100+
</UiText>
101+
</label>
102+
</div>
103+
104+
<div class="grid shrink-0 gap-3 md:grid-cols-2">
105+
<div class="space-y-2">
106+
<UiHeading
107+
:title="i18n.t('devtools:compare.jsonDiff.original')"
108+
:level="3"
109+
/>
110+
<DevtoolsCompareJsonDiffInput
111+
v-model="leftText"
112+
:placeholder="i18n.t('devtools:form.placeholder.text')"
113+
:error="localizedError(leftError)"
114+
:scroll-top="leftTargetScrollTop"
115+
:scroll-left="leftTargetScrollLeft"
116+
@paste="scheduleLeftFormat"
117+
@blur="formatLeftOnBlur"
118+
@scroll="syncLeftEditorScroll"
119+
/>
120+
</div>
121+
<div class="space-y-2">
122+
<UiHeading
123+
:title="i18n.t('devtools:compare.jsonDiff.modified')"
124+
:level="3"
125+
/>
126+
<DevtoolsCompareJsonDiffInput
127+
v-model="rightText"
128+
:placeholder="i18n.t('devtools:form.placeholder.text')"
129+
:error="localizedError(rightError)"
130+
:scroll-top="rightTargetScrollTop"
131+
:scroll-left="rightTargetScrollLeft"
132+
@paste="scheduleRightFormat"
133+
@blur="formatRightOnBlur"
134+
@scroll="syncRightEditorScroll"
135+
/>
136+
</div>
137+
</div>
138+
139+
<UiText
140+
v-if="viewerError"
141+
as="div"
142+
class="text-destructive"
143+
>
144+
{{ localizedError(viewerError) }}
145+
</UiText>
146+
147+
<DevtoolsCompareJsonDiffViewer
148+
v-else-if="isReady"
149+
class="min-h-0 flex-1"
150+
:nodes="nodes"
151+
:is-expanded="isExpanded"
152+
@toggle="toggleExpanded"
153+
/>
154+
155+
<UiText
156+
v-else-if="showEmptyState"
157+
as="div"
158+
muted
159+
class="flex flex-1 items-center justify-center py-8 text-center"
160+
>
161+
{{ i18n.t("devtools:compare.jsonDiff.empty") }}
162+
</UiText>
163+
</div>
164+
</template>

0 commit comments

Comments
 (0)