@@ -15,13 +15,15 @@ import {
1515 defaultHighlightStyle ,
1616 foldKeymap ,
1717 foldGutter ,
18+ codeFolding ,
1819 indentOnInput ,
1920 bracketMatching ,
2021 syntaxHighlighting ,
2122} from "@codemirror/language"
2223import { tags as t } from "@lezer/highlight"
2324import { Extension , EditorState } from "@codemirror/state"
2425import { history , historyKeymap , defaultKeymap } from "@codemirror/commands"
26+
2527import {
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
328335const 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+
400538export 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