@@ -25,9 +25,9 @@ const defaultConfig: Required<IndentGuidesConfig> = {
2525} ;
2626
2727const GUIDE_MARK_CLASS = "cm-indent-guides" ;
28- const MAX_VISIBLE_GUIDE_LINES = 1200 ;
28+ const GUIDE_LINE_CLASS = "cm-indent-guides-line" ;
29+ const MAX_VISIBLE_GUIDE_LINES = 500 ;
2930const MAX_GUIDE_LEVELS = 40 ;
30- const REBUILD_DELAY_MS = 50 ;
3131
3232interface IndentLineInfo {
3333 text : string ;
@@ -40,6 +40,8 @@ interface IndentLineInfo {
4040type IndentLineCache = Map < number , IndentLineInfo > ;
4141type GuideStyleCache = Map < string , string > ;
4242
43+ const BLANK_LINE_SCAN_LIMIT = 100 ;
44+
4345/**
4446 * Get the tab size from editor state
4547 */
@@ -171,8 +173,47 @@ function buildDecorations(
171173 for ( const { from : blockFrom , to : blockTo } of view . visibleRanges ) {
172174 const startLine = state . doc . lineAt ( blockFrom ) ;
173175 const endLine = state . doc . lineAt ( blockTo ) ;
176+ const firstLineNumber = startLine . number ;
177+ const lastLineNumber = endLine . number ;
178+ const scanStartLine = Math . max ( 1 , firstLineNumber - BLANK_LINE_SCAN_LIMIT ) ;
179+ const scanEndLine = Math . min (
180+ state . doc . lines ,
181+ lastLineNumber + BLANK_LINE_SCAN_LIMIT ,
182+ ) ;
183+ const prevIndentByLine = new Map < number , number > ( ) ;
184+ const nextIndentByLine = new Map < number , number > ( ) ;
185+ let prevIndent = - 1 ;
186+ let prevIndentLine = - 1 ;
187+
188+ for ( let lineNum = scanStartLine ; lineNum <= scanEndLine ; lineNum ++ ) {
189+ const line = state . doc . line ( lineNum ) ;
190+ const info = getCachedLineInfo ( lineNum , line . text , tabSize , lineCache ) ;
191+ prevIndentByLine . set (
192+ lineNum ,
193+ lineNum - prevIndentLine <= BLANK_LINE_SCAN_LIMIT ? prevIndent : - 1 ,
194+ ) ;
195+ if ( ! info . blank ) {
196+ prevIndent = info . indentColumns ;
197+ prevIndentLine = lineNum ;
198+ }
199+ }
200+
201+ let nextIndent = - 1 ;
202+ let nextIndentLine = state . doc . lines + 1 ;
203+ for ( let lineNum = scanEndLine ; lineNum >= scanStartLine ; lineNum -- ) {
204+ const line = state . doc . line ( lineNum ) ;
205+ const info = getCachedLineInfo ( lineNum , line . text , tabSize , lineCache ) ;
206+ nextIndentByLine . set (
207+ lineNum ,
208+ nextIndentLine - lineNum <= BLANK_LINE_SCAN_LIMIT ? nextIndent : - 1 ,
209+ ) ;
210+ if ( ! info . blank ) {
211+ nextIndent = info . indentColumns ;
212+ nextIndentLine = lineNum ;
213+ }
214+ }
174215
175- for ( let lineNum = startLine . number ; lineNum <= endLine . number ; lineNum ++ ) {
216+ for ( let lineNum = firstLineNumber ; lineNum <= lastLineNumber ; lineNum ++ ) {
176217 if ( processedLines >= MAX_VISIBLE_GUIDE_LINES ) return builder . finish ( ) ;
177218 processedLines ++ ;
178219
@@ -183,23 +224,49 @@ function buildDecorations(
183224 continue ;
184225 }
185226
227+ let indentColumns = info . indentColumns ;
228+ if ( info . blank ) {
229+ const previousIndent = prevIndentByLine . get ( lineNum ) ?? - 1 ;
230+ const followingIndent = nextIndentByLine . get ( lineNum ) ?? - 1 ;
231+ if ( previousIndent !== - 1 && followingIndent !== - 1 ) {
232+ indentColumns = Math . min ( previousIndent , followingIndent ) ;
233+ } else if ( previousIndent !== - 1 ) {
234+ indentColumns = previousIndent ;
235+ } else if ( followingIndent !== - 1 ) {
236+ indentColumns = followingIndent ;
237+ }
238+ }
239+
186240 const levels = Math . min (
187- Math . floor ( info . indentColumns / indentUnit ) ,
241+ Math . floor ( indentColumns / indentUnit ) ,
188242 MAX_GUIDE_LEVELS ,
189243 ) ;
190244 if ( levels <= 0 ) continue ;
191- if ( info . leadingWhitespaceLength <= 0 ) continue ;
192-
193- builder . add (
194- line . from ,
195- line . from + info . leadingWhitespaceLength ,
196- Decoration . mark ( {
197- attributes : {
198- class : GUIDE_MARK_CLASS ,
199- style : getGuideStyle ( levels , guideStepPx , styleCache ) ,
200- } ,
201- } ) ,
202- ) ;
245+
246+ if ( info . blank ) {
247+ builder . add (
248+ line . from ,
249+ line . from ,
250+ Decoration . line ( {
251+ attributes : {
252+ class : GUIDE_LINE_CLASS ,
253+ style : getGuideStyle ( levels , guideStepPx , styleCache ) ,
254+ } ,
255+ } ) ,
256+ ) ;
257+ } else {
258+ if ( info . leadingWhitespaceLength <= 0 ) continue ;
259+ builder . add (
260+ line . from ,
261+ line . from + info . leadingWhitespaceLength ,
262+ Decoration . mark ( {
263+ attributes : {
264+ class : GUIDE_MARK_CLASS ,
265+ style : getGuideStyle ( levels , guideStepPx , styleCache ) ,
266+ } ,
267+ } ) ,
268+ ) ;
269+ }
203270 }
204271 }
205272
@@ -218,57 +285,72 @@ function createIndentGuidesPlugin(
218285 return ViewPlugin . fromClass (
219286 class {
220287 decorations : DecorationSet ;
221- rebuildTimer = 0 ;
222- pendingView : EditorView | null = null ;
223288 lineCache : IndentLineCache = new Map ( ) ;
224289 styleCache : GuideStyleCache = new Map ( ) ;
290+ lastCharWidth = 0 ;
291+ lastTabSize = 4 ;
292+ lastIndentUnit = 4 ;
225293
226294 constructor ( view : EditorView ) {
227- this . decorations = Decoration . none ;
228- this . scheduleBuild ( view ) ;
295+ const { state } = view ;
296+ this . lastCharWidth = view . defaultCharacterWidth ;
297+ this . lastTabSize = getTabSize ( state ) ;
298+ this . lastIndentUnit = getIndentUnitColumns ( state ) ;
299+
300+ this . decorations = buildDecorations (
301+ view ,
302+ config ,
303+ this . lineCache ,
304+ this . styleCache ,
305+ ) ;
229306 }
230307
231308 update ( update : ViewUpdate ) : void {
232- if (
233- ! update . docChanged &&
234- ! update . viewportChanged &&
235- ! update . geometryChanged
236- ) {
237- return ;
238- }
309+ const { view, state } = update ;
310+ let needsRebuild = false ;
311+
239312 if ( update . docChanged ) {
240313 this . decorations = this . decorations . map ( update . changes ) ;
241314 this . lineCache . clear ( ) ;
315+ needsRebuild = true ;
316+ }
317+
318+ if ( update . viewportChanged ) {
319+ needsRebuild = true ;
320+ }
321+
322+ const currentTabSize = getTabSize ( state ) ;
323+ const currentIndentUnit = getIndentUnitColumns ( state ) ;
324+ const currentCharWidth = view . defaultCharacterWidth ;
325+
326+ if (
327+ currentTabSize !== this . lastTabSize ||
328+ currentIndentUnit !== this . lastIndentUnit
329+ ) {
330+ this . lastTabSize = currentTabSize ;
331+ this . lastIndentUnit = currentIndentUnit ;
332+ this . lineCache . clear ( ) ;
333+ this . styleCache . clear ( ) ;
334+ needsRebuild = true ;
242335 }
243- if ( update . geometryChanged ) {
336+
337+ if ( currentCharWidth !== this . lastCharWidth ) {
338+ this . lastCharWidth = currentCharWidth ;
244339 this . styleCache . clear ( ) ;
340+ needsRebuild = true ;
245341 }
246- this . scheduleBuild ( update . view ) ;
247- }
248342
249- scheduleBuild ( view : EditorView ) : void {
250- this . pendingView = view ;
251- if ( this . rebuildTimer ) return ;
252- this . rebuildTimer = window . setTimeout ( ( ) => {
253- this . rebuildTimer = 0 ;
254- const pendingView = this . pendingView ;
255- this . pendingView = null ;
256- if ( ! pendingView ) return ;
343+ if ( needsRebuild ) {
257344 this . decorations = buildDecorations (
258- pendingView ,
345+ view ,
259346 config ,
260347 this . lineCache ,
261348 this . styleCache ,
262349 ) ;
263- } , REBUILD_DELAY_MS ) ;
350+ }
264351 }
265352
266353 destroy ( ) : void {
267- if ( this . rebuildTimer ) {
268- window . clearTimeout ( this . rebuildTimer ) ;
269- this . rebuildTimer = 0 ;
270- }
271- this . pendingView = null ;
272354 this . lineCache . clear ( ) ;
273355 this . styleCache . clear ( ) ;
274356 }
@@ -288,6 +370,9 @@ const indentGuidesTheme = EditorView.baseTheme({
288370 display : "inline-block" ,
289371 verticalAlign : "top" ,
290372 } ,
373+ ".cm-indent-guides-line" : {
374+ backgroundOrigin : "content-box" ,
375+ } ,
291376 "&" : {
292377 "--indent-guide-color" : "rgba(128, 128, 128, 0.25)" ,
293378 } ,
0 commit comments