Skip to content

Commit 11799c1

Browse files
jarvis2754nivedin
andauthored
feat: add structured JSON fold indicators in response viewer (hoppscotch#5347)
Co-authored-by: nivedin <[email protected]>
1 parent 430d6d3 commit 11799c1

File tree

1 file changed

+139
-0
lines changed
  • packages/hoppscotch-common/src/helpers/editor/themes

1 file changed

+139
-0
lines changed

packages/hoppscotch-common/src/helpers/editor/themes/baseTheme.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,15 @@ import {
1515
defaultHighlightStyle,
1616
foldKeymap,
1717
foldGutter,
18+
codeFolding,
1819
indentOnInput,
1920
bracketMatching,
2021
syntaxHighlighting,
2122
} from "@codemirror/language"
2223
import { tags as t } from "@lezer/highlight"
2324
import { Extension, EditorState } from "@codemirror/state"
2425
import { history, historyKeymap, defaultKeymap } from "@codemirror/commands"
26+
2527
import {
2628
closeBrackets,
2729
closeBracketsKeymap,
@@ -323,6 +325,11 @@ export const inputTheme = EditorView.theme({
323325
color: "var(--secondary-dark-color)",
324326
borderColor: "var(--divider-dark-color)",
325327
},
328+
".cm-jsonFoldSummary": {
329+
opacity: "0.7",
330+
fontStyle: "italic",
331+
background: "var(--divider-dark-color)",
332+
},
326333
})
327334

328335
const editorTypeColor = "var(--editor-type-color)"
@@ -397,6 +404,137 @@ export const baseHighlightStyle = HighlightStyle.define([
397404
{ tag: t.invalid, color: editorInvalidColor },
398405
])
399406

407+
/**
408+
* Generic body counter (array or object).
409+
*
410+
* @param body - String content inside `[...]` or `{...}`.
411+
* @param trigger - The character that indicates a top-level separator (`,` or `:`).
412+
* @param finalize - Function to adjust the final count (e.g., add +1 for arrays).
413+
*/
414+
function countBodyUnits(
415+
body: string,
416+
trigger: string,
417+
finalize: (count: number, sawContent: boolean) => number
418+
): number {
419+
let inString = false
420+
let escape = false
421+
let bracketDepth = 0
422+
let braceDepth = 0
423+
let count = 0
424+
let sawContent = false
425+
426+
for (let i = 0; i < body.length; i++) {
427+
const ch = body[i]
428+
429+
if (escape) {
430+
escape = false
431+
continue
432+
}
433+
if (ch === "\\") {
434+
escape = true
435+
continue
436+
}
437+
if (ch === '"') {
438+
inString = !inString
439+
continue
440+
}
441+
if (inString) continue
442+
443+
if (ch === "[") bracketDepth++
444+
else if (ch === "]") bracketDepth--
445+
else if (ch === "{") braceDepth++
446+
else if (ch === "}") braceDepth--
447+
448+
if (!sawContent && !/\s/.test(ch)) sawContent = true
449+
450+
if (ch === trigger && bracketDepth === 0 && braceDepth === 0) {
451+
count++
452+
}
453+
}
454+
455+
return finalize(count, sawContent)
456+
}
457+
458+
/**
459+
* Count the number of top-level items in an array body string.
460+
*/
461+
function countArrayItemsInBody(body: string): number {
462+
return countBodyUnits(body, ",", (count, sawContent) =>
463+
!sawContent ? 0 : count + 1
464+
)
465+
}
466+
467+
/**
468+
* Count the number of top-level fields in an object body string.
469+
*/
470+
function countObjectFieldsInBody(body: string): number {
471+
return countBodyUnits(body, ":", (count) => count)
472+
}
473+
474+
/**
475+
* Compute a fold summary string for a JSON range.
476+
*
477+
* @param state - Current editor state
478+
* @param from - Start position of the fold
479+
* @param to - End position of the fold
480+
*/
481+
function computeJsonSummary(
482+
state: EditorState,
483+
from: number,
484+
to: number
485+
): string {
486+
const docLength = state.doc.length
487+
const sliceFrom = Math.max(0, from - 1)
488+
const sliceTo = Math.min(docLength, to + 1)
489+
const slice = state.sliceDoc(sliceFrom, sliceTo)
490+
491+
// Indices relative to slice
492+
const textStart = from - sliceFrom
493+
const textEnd = textStart + (to - from)
494+
495+
const text = slice.substring(textStart, textEnd).trim()
496+
const prevChar = from > 0 ? slice.charAt(textStart - 1) : ""
497+
const nextChar = textEnd < slice.length ? slice.charAt(textEnd) : ""
498+
499+
// Try full JSON parse first (works if selection is a valid value)
500+
try {
501+
const parsed = JSON.parse(text)
502+
if (Array.isArray(parsed)) {
503+
return `[ … ] (${parsed.length} items)`
504+
}
505+
if (parsed && typeof parsed === "object") {
506+
return `{ … } (${Object.keys(parsed).length} fields)`
507+
}
508+
} catch {
509+
// Not standalone JSON → fallback to counting
510+
}
511+
512+
// Infer container type by surrounding characters
513+
if (prevChar === "[" || nextChar === "]") {
514+
return `[ … ] (${countArrayItemsInBody(text)} items)`
515+
}
516+
if (prevChar === "{" || nextChar === "}" || text.includes(":")) {
517+
return `{ … } (${countObjectFieldsInBody(text)} fields)`
518+
}
519+
520+
return "…"
521+
}
522+
523+
/**
524+
* Extension: JSON folding with informative summaries.
525+
*/
526+
export const jsonFoldSummary: Extension = codeFolding({
527+
preparePlaceholder: (state, range) =>
528+
computeJsonSummary(state, range.from, range.to),
529+
placeholderDOM: (view, onclick, prepared) => {
530+
const span = document.createElement("span")
531+
span.className = "cm-foldPlaceholder cm-jsonFoldSummary"
532+
span.textContent = typeof prepared === "string" ? prepared : "…"
533+
span.addEventListener("click", onclick)
534+
return span
535+
},
536+
})
537+
400538
export const basicSetup: Extension = [
401539
lineNumbers(),
402540
highlightActiveLineGutter(),
@@ -406,6 +544,7 @@ export const basicSetup: Extension = [
406544
openText: "▾",
407545
closedText: "▸",
408546
}),
547+
jsonFoldSummary,
409548
drawSelection(),
410549
dropCursor(),
411550
EditorState.allowMultipleSelections.of(true),

0 commit comments

Comments
 (0)