@@ -6,13 +6,20 @@ import {
66 foldNodeProp ,
77 foldService ,
88 foldState ,
9+ forceParsing ,
10+ language ,
11+ StreamLanguage ,
912 syntaxTree ,
13+ syntaxTreeAvailable ,
1014 unfoldEffect ,
1115} from "@codemirror/language" ;
1216import { StateEffect } from "@codemirror/state" ;
1317
14- const FULL_PARSE_BUDGET_MS = 100 ;
15- const INCOMPLETE_PARSE_MARGIN = 50 ;
18+ const INITIAL_PARSE_BUDGET_MS = 12 ;
19+ const IDLE_PARSE_BUDGET_MS = 10 ;
20+ const MIN_IDLE_PARSE_BUDGET_MS = 2 ;
21+ const IDLE_CALLBACK_TIMEOUT_MS = 50 ;
22+ const pendingFoldAllJobs = new WeakMap ( ) ;
1623
1724function findServiceFold ( state , line ) {
1825 for ( const service of state . facet ( foldService ) ) {
@@ -22,68 +29,104 @@ function findServiceFold(state, line) {
2229 return null ;
2330}
2431
25- function addSyntaxFolds ( state , tree , line , addRange ) {
26- if ( ! tree || tree . length < line . to ) return ;
32+ function scheduleIdleWork ( callback ) {
33+ if (
34+ typeof window !== "undefined" &&
35+ typeof window . requestIdleCallback === "function"
36+ ) {
37+ return {
38+ kind : "idle" ,
39+ id : window . requestIdleCallback ( callback , {
40+ timeout : IDLE_CALLBACK_TIMEOUT_MS ,
41+ } ) ,
42+ } ;
43+ }
2744
28- for ( let iter = tree . resolveStack ( line . to , 1 ) ; iter ; iter = iter . next ) {
29- const node = iter . node ;
30- if ( node . to <= line . to || node . from > line . to ) continue ;
31- const lastChild = node . lastChild ;
32- if (
33- tree . length !== state . doc . length &&
34- node . to >= tree . length - INCOMPLETE_PARSE_MARGIN &&
35- lastChild ?. to === node . to &&
36- lastChild . type . isError
37- ) {
38- continue ;
39- }
45+ return {
46+ kind : "timeout" ,
47+ id : setTimeout ( ( ) => callback ( null ) , 0 ) ,
48+ } ;
49+ }
4050
41- const fold = node . type . prop ( foldNodeProp ) ;
42- if ( ! fold ) continue ;
51+ function cancelIdleWork ( handle ) {
52+ if ( ! handle ) return ;
53+ if (
54+ handle . kind === "idle" &&
55+ typeof window !== "undefined" &&
56+ typeof window . cancelIdleCallback === "function"
57+ ) {
58+ window . cancelIdleCallback ( handle . id ) ;
59+ } else {
60+ clearTimeout ( handle . id ) ;
61+ }
62+ }
4363
44- const range = fold ( node , state ) ;
45- if (
46- range &&
47- range . from >= line . from &&
48- range . from <= line . to &&
49- range . to > line . to
50- ) {
51- addRange ( range ) ;
52- }
64+ function cancelPendingFoldAll ( view ) {
65+ const job = pendingFoldAllJobs . get ( view ) ;
66+ if ( ! job ) return ;
67+ job . cancelled = true ;
68+ cancelIdleWork ( job . handle ) ;
69+ pendingFoldAllJobs . delete ( view ) ;
70+ }
71+
72+ function getIdleParseBudget ( deadline ) {
73+ if ( ! deadline || typeof deadline . timeRemaining !== "function" ) {
74+ return IDLE_PARSE_BUDGET_MS ;
5375 }
76+ return Math . max (
77+ MIN_IDLE_PARSE_BUDGET_MS ,
78+ Math . min ( IDLE_PARSE_BUDGET_MS , Math . floor ( deadline . timeRemaining ( ) ) ) ,
79+ ) ;
5480}
5581
56- /**
57- * Fold every foldable block, including nested blocks. CodeMirror's built-in
58- * foldAll intentionally skips nested ranges after finding a top-level fold.
59- */
60- export function foldAllCodeBlocks ( view ) {
61- const { state } = view ;
62- const tree =
63- ensureSyntaxTree ( state , state . doc . length , FULL_PARSE_BUDGET_MS ) ??
64- syntaxTree ( state ) ;
82+ function addSyntaxFolds ( state , tree , addRange ) {
83+ if ( ! tree || tree . length < state . doc . length ) return ;
84+
85+ tree . iterate ( {
86+ enter ( ref ) {
87+ const node = ref . node ;
88+ const fold = node . type . prop ( foldNodeProp ) ;
89+ if ( ! fold ) return ;
90+
91+ const range = fold ( node , state ) ;
92+ if ( ! range ) return ;
6593
94+ const line = state . doc . lineAt ( range . from ) ;
95+ if ( range . from <= line . to && range . to > line . to ) addRange ( range ) ;
96+ } ,
97+ } ) ;
98+ }
99+
100+ function collectFoldEffects ( state , tree ) {
66101 const existing = new Set ( ) ;
67102 foldedRanges ( state ) . between ( 0 , state . doc . length , ( from , to ) => {
68103 existing . add ( `${ from } :${ to } ` ) ;
69104 } ) ;
70105
71- const effects = [ ] ;
106+ const ranges = [ ] ;
72107 const discovered = new Set ( ) ;
73108 const addRange = ( range ) => {
74109 const id = `${ range . from } :${ range . to } ` ;
75110 if ( existing . has ( id ) || discovered . has ( id ) ) return ;
76111 discovered . add ( id ) ;
77- effects . push ( foldEffect . of ( range ) ) ;
112+ ranges . push ( range ) ;
78113 } ;
79114
80115 for ( let lineNumber = 1 ; lineNumber <= state . doc . lines ; lineNumber += 1 ) {
81116 const line = state . doc . line ( lineNumber ) ;
82117 const serviceRange = findServiceFold ( state , line ) ;
83118 if ( serviceRange ) addRange ( serviceRange ) ;
84- else addSyntaxFolds ( state , tree , line , addRange ) ;
85119 }
120+ addSyntaxFolds ( state , tree , addRange ) ;
121+
122+ return ranges
123+ . sort ( ( a , b ) => a . from - b . from || a . to - b . to )
124+ . map ( ( range ) => foldEffect . of ( range ) ) ;
125+ }
86126
127+ function applyAllFolds ( view , tree ) {
128+ const { state } = view ;
129+ const effects = collectFoldEffects ( state , tree ) ;
87130 if ( ! effects . length ) return false ;
88131 if ( ! state . field ( foldState , false ) ) {
89132 // Install the state field first so it can consume the fold effects in
@@ -94,8 +137,75 @@ export function foldAllCodeBlocks(view) {
94137 return true ;
95138}
96139
140+ function scheduleProgressiveFoldAll ( view , document ) {
141+ const job = {
142+ cancelled : false ,
143+ document,
144+ handle : null ,
145+ } ;
146+ pendingFoldAllJobs . set ( view , job ) ;
147+
148+ const continueParsing = ( deadline ) => {
149+ job . handle = null ;
150+ if (
151+ job . cancelled ||
152+ pendingFoldAllJobs . get ( view ) !== job ||
153+ view . state . doc !== document
154+ ) {
155+ pendingFoldAllJobs . delete ( view ) ;
156+ return ;
157+ }
158+
159+ try {
160+ const complete =
161+ syntaxTreeAvailable ( view . state , document . length ) ||
162+ forceParsing ( view , document . length , getIdleParseBudget ( deadline ) ) ;
163+
164+ if ( complete ) {
165+ pendingFoldAllJobs . delete ( view ) ;
166+ applyAllFolds ( view , syntaxTree ( view . state ) ) ;
167+ return ;
168+ }
169+ } catch ( error ) {
170+ console . error ( "Failed to finish parsing for Fold all." , error ) ;
171+ pendingFoldAllJobs . delete ( view ) ;
172+ return ;
173+ }
174+
175+ job . handle = scheduleIdleWork ( continueParsing ) ;
176+ } ;
177+
178+ job . handle = scheduleIdleWork ( continueParsing ) ;
179+ }
180+
181+ /**
182+ * Fold every foldable block, including nested blocks. Small and already-parsed
183+ * documents fold synchronously. Large documents finish parsing in short idle
184+ * slices before all ranges are collected in one pass and one transaction.
185+ */
186+ export function foldAllCodeBlocks ( view ) {
187+ cancelPendingFoldAll ( view ) ;
188+ const { state } = view ;
189+ const activeLanguage = state . facet ( language ) ;
190+
191+ if ( ! activeLanguage || activeLanguage instanceof StreamLanguage ) {
192+ return applyAllFolds ( view , syntaxTree ( state ) ) ;
193+ }
194+
195+ const tree = ensureSyntaxTree (
196+ state ,
197+ state . doc . length ,
198+ INITIAL_PARSE_BUDGET_MS ,
199+ ) ;
200+ if ( tree ) return applyAllFolds ( view , tree ) ;
201+
202+ scheduleProgressiveFoldAll ( view , state . doc ) ;
203+ return true ;
204+ }
205+
97206/** Unfold every stored fold, including nested folds created above. */
98207export function unfoldAllCodeBlocks ( view ) {
208+ cancelPendingFoldAll ( view ) ;
99209 const effects = [ ] ;
100210 foldedRanges ( view . state ) . between ( 0 , view . state . doc . length , ( from , to ) => {
101211 effects . push ( unfoldEffect . of ( { from, to } ) ) ;
0 commit comments