Skip to content

Commit f3023f7

Browse files
author
cahierchen
committed
feat: add batch folding with progress indicator for large JSON files
1 parent ebfe0e6 commit f3023f7

File tree

3 files changed

+131
-6
lines changed

3 files changed

+131
-6
lines changed

src/lib/components/__snapshots__/JSONEditor.test.ts.snap

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -626,7 +626,7 @@ exports[`JSONEditor > render text mode 1`] = `
626626
>
627627
<!---->
628628
<div
629-
class="jse-text-mode svelte-xt61xw"
629+
class="jse-text-mode svelte-1ovs4jk"
630630
>
631631
<div
632632
class="jse-menu svelte-pf7s2l"
@@ -921,8 +921,10 @@ exports[`JSONEditor > render text mode 1`] = `
921921
</div>
922922
<!---->
923923
924+
<!---->
925+
924926
<div
925-
class="jse-contents svelte-xt61xw"
927+
class="jse-contents svelte-1ovs4jk"
926928
>
927929
<div
928930
class="cm-editor ͼ1 ͼ2 ͼ4 ͼt"

src/lib/components/modes/textmode/TextMode.scss

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,4 +191,49 @@
191191
padding: 2px;
192192
}
193193
}
194+
195+
.jse-fold-progress {
196+
display: flex;
197+
align-items: center;
198+
gap: 8px;
199+
padding: 8px 12px;
200+
background: defaults.$background-color;
201+
border-top: defaults.$panel-border;
202+
border-bottom: defaults.$panel-border;
203+
204+
.jse-fold-progress-track {
205+
flex: 1;
206+
height: 6px;
207+
background: defaults.$panel-background;
208+
border-radius: 3px;
209+
overflow: hidden;
210+
border: 1px solid defaults.$panel-border;
211+
}
212+
213+
.jse-fold-progress-fill {
214+
height: 100%;
215+
background: linear-gradient(90deg, defaults.$theme-color, defaults.$theme-color-highlight);
216+
border-radius: 2px;
217+
transition: width 0.1s ease;
218+
min-width: 2px;
219+
}
220+
221+
.jse-fold-cancel-button {
222+
padding: 4px 12px;
223+
font-size: 12px;
224+
font-family: defaults.$font-family;
225+
background: defaults.$theme-color;
226+
color: defaults.$white;
227+
border-radius: 3px;
228+
cursor: pointer;
229+
transition: background-color 0.2s ease;
230+
flex-shrink: 0;
231+
border: 1px solid defaults.$main-border;
232+
233+
&:hover {
234+
background: defaults.$theme-color-highlight;
235+
color: defaults.$white;
236+
}
237+
}
238+
}
194239
}

src/lib/components/modes/textmode/TextMode.svelte

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,14 @@
181181
let askToFormatApplied = askToFormat
182182
183183
let validationErrors: ValidationError[] = []
184+
185+
// collapse state
186+
let isFolding = false
187+
let foldProgress = 0
188+
let foldTotal = 0
189+
let foldCancelController: AbortController | null = null
190+
const collapseBatchSize = 100 // number of ranges to collapse in one batch
191+
const showCollapseProgressSize = 5000 // if the number of ranges is larger than this, show progress
184192
const linterCompartment = new Compartment()
185193
const readOnlyCompartment = new Compartment()
186194
const indentCompartment = new Compartment()
@@ -278,6 +286,7 @@
278286
debug('Destroy CodeMirror editor')
279287
codeMirrorView.destroy()
280288
}
289+
handleCancelFolding()
281290
})
282291
283292
const sortModalId = uniqueId()
@@ -376,6 +385,54 @@
376385
377386
return foldRanges
378387
}
388+
async function foldRangesAnimated(foldRanges: { from: number; to: number }[]) {
389+
if (foldRanges.length === 0) return
390+
391+
// if the number of ranges is large, show progress
392+
const shouldShowProgress = foldRanges.length > showCollapseProgressSize
393+
394+
if (shouldShowProgress) {
395+
isFolding = true
396+
foldProgress = 0
397+
foldTotal = foldRanges.length
398+
foldCancelController = new AbortController()
399+
}
400+
401+
const processBatch = (startIndex: number): Promise<void> => {
402+
return new Promise<void>((resolve) => {
403+
// check if folding was cancelled
404+
if (shouldShowProgress && foldCancelController?.signal.aborted) {
405+
resolve()
406+
return
407+
}
408+
requestAnimationFrame(() => {
409+
const endIndex = Math.min(startIndex + collapseBatchSize, foldRanges.length)
410+
const batch = foldRanges.slice(startIndex, endIndex)
411+
codeMirrorView.dispatch({
412+
effects: batch.map((range) => foldEffect.of({ from: range.from, to: range.to }))
413+
})
414+
415+
if (shouldShowProgress) {
416+
foldProgress = endIndex
417+
}
418+
419+
if (endIndex < foldRanges.length) {
420+
processBatch(endIndex).then(resolve)
421+
} else {
422+
resolve()
423+
}
424+
})
425+
})
426+
}
427+
428+
await processBatch(0)
429+
if (shouldShowProgress) {
430+
isFolding = false
431+
foldProgress = 0
432+
foldTotal = 0
433+
foldCancelController = null
434+
}
435+
}
379436
380437
function foldRecursive(path: JSONPath = [], recursive: boolean = true) {
381438
const state = codeMirrorView.state
@@ -397,9 +454,13 @@
397454
}
398455
399456
if (foldRanges.length > 0) {
400-
codeMirrorView.dispatch({
401-
effects: foldRanges.map((range) => foldEffect.of({ from: range.from, to: range.to }))
402-
})
457+
foldRangesAnimated(foldRanges)
458+
}
459+
}
460+
461+
function handleCancelFolding() {
462+
if (foldCancelController) {
463+
foldCancelController.abort()
403464
}
404465
}
405466
@@ -1273,7 +1334,24 @@
12731334
{onRenderMenu}
12741335
/>
12751336
{/if}
1276-
1337+
{#if isFolding}
1338+
<div class="jse-fold-progress">
1339+
<div class="jse-fold-progress-track">
1340+
<div
1341+
class="jse-fold-progress-fill"
1342+
style="width: {foldTotal > 0 ? (foldProgress / foldTotal) * 100 : 0}%"
1343+
></div>
1344+
</div>
1345+
<button
1346+
class="jse-fold-cancel-button"
1347+
type="button"
1348+
title="Cancel folding"
1349+
on:click={handleCancelFolding}
1350+
>
1351+
Cancel
1352+
</button>
1353+
</div>
1354+
{/if}
12771355
{#if !isSSR}
12781356
{@const editorDisabled = disableTextEditor(text, acceptTooLarge)}
12791357

0 commit comments

Comments
 (0)