diff --git a/src/editor/codemirror/structure-highlighting/theme.ts b/src/editor/codemirror/structure-highlighting/theme.ts index 353f85a07..5eac46d64 100644 --- a/src/editor/codemirror/structure-highlighting/theme.ts +++ b/src/editor/codemirror/structure-highlighting/theme.ts @@ -13,8 +13,10 @@ export const baseTheme = EditorView.baseTheme({ position: "absolute", top: 0, height: "100%", - width: "100%", + // Width is set in code. zIndex: -1, + // Prevents horizontal scrollbar flicker when narrowing the editor. + overflow: "hidden", }, ".cm-cs--block, .cm-cs--indent": { display: "block", diff --git a/src/editor/codemirror/structure-highlighting/view.ts b/src/editor/codemirror/structure-highlighting/view.ts index 7c9e4d9b2..7daff91e5 100644 --- a/src/editor/codemirror/structure-highlighting/view.ts +++ b/src/editor/codemirror/structure-highlighting/view.ts @@ -25,8 +25,27 @@ const grammarInfo = { ]), }; -interface Measure { - blocks: VisualBlock[]; +class Measure { + constructor( + readonly width: number, + readonly left: number, + readonly blocks: VisualBlock[] + ) {} + eq(other: Measure) { + if (this.width !== other.width) { + return false; + } + if (this.left !== other.left) { + return false; + } + const blocksChanged = + other.blocks.length !== this.blocks.length || + other.blocks.some((b, i) => !b.eq(this.blocks[i])); + if (blocksChanged) { + return false; + } + return true; + } } export const codeStructureView = (option: "full" | "simple") => @@ -34,7 +53,7 @@ export const codeStructureView = (option: "full" | "simple") => class { measureReq: { read: () => Measure; write: (value: Measure) => void }; overlayLayer: HTMLElement; - blocks: VisualBlock[] = []; + lastMeasure: Measure = new Measure(0, 0, []); constructor(readonly view: EditorView) { this.measureReq = { @@ -59,6 +78,18 @@ export const codeStructureView = (option: "full" | "simple") => const view = this.view; const { state } = view; + const contentDOMRect = view.contentDOM.getBoundingClientRect(); + const scrollDOMRect = view.scrollDOM.getBoundingClientRect(); + // The gutter is awkward as it's position fixed in the scroller. + const gutterWidth = + view.scrollDOM.firstElementChild!.getBoundingClientRect().width; + const width = Math.max( + contentDOMRect.width, + // When the content is narrower than the scrollable area. + scrollDOMRect.width - gutterWidth + ); + const left = gutterWidth; + let cursorFound = false; const positionsForNode = ( @@ -67,11 +98,8 @@ export const codeStructureView = (option: "full" | "simple") => end: number, depth: number, body: boolean - ) => { + ): Positions | undefined => { const diagnostics = state.field(lintState, false)?.diagnostics; - const leftEdge = - view.contentDOM.getBoundingClientRect().left - - view.scrollDOM.getBoundingClientRect().left; const indentWidth = state.facet(indentUnit).length * view.defaultCharacterWidth; @@ -106,7 +134,7 @@ export const codeStructureView = (option: "full" | "simple") => const bottom = bottomLine.bottom; const height = bottom - top; const leftIndent = depth * indentWidth; - const left = leftEdge + leftIndent; + const left = leftIndent; const mainCursor = state.selection.main.head; const cursorActive = !cursorFound && @@ -170,6 +198,7 @@ export const codeStructureView = (option: "full" | "simple") => blocks.push( new VisualBlock( bodyPullBack, + width, parentPositions, bodyPositions ) @@ -191,15 +220,16 @@ export const codeStructureView = (option: "full" | "simple") => }, }); } - return { blocks: blocks.reverse() }; + return new Measure(width, left, blocks.reverse()); } - drawBlocks({ blocks }: Measure) { - const blocksChanged = - blocks.length !== this.blocks.length || - blocks.some((b, i) => !b.eq(this.blocks[i])); - if (blocksChanged) { - this.blocks = blocks; + drawBlocks(measure: Measure) { + if (!measure.eq(this.lastMeasure)) { + const { blocks, left, width } = measure; + this.lastMeasure = measure; + + this.overlayLayer.style.width = width + "px"; + this.overlayLayer.style.left = left + "px"; // Should be able to adjust old elements here if it's a performance win. this.overlayLayer.textContent = ""; diff --git a/src/editor/codemirror/structure-highlighting/visual-block.ts b/src/editor/codemirror/structure-highlighting/visual-block.ts index 1a206e830..410404c7b 100644 --- a/src/editor/codemirror/structure-highlighting/visual-block.ts +++ b/src/editor/codemirror/structure-highlighting/visual-block.ts @@ -39,6 +39,7 @@ export class Positions { export class VisualBlock { constructor( readonly bodyPullBack: boolean, + readonly width: number, readonly parent?: Positions, readonly body?: Positions ) {} @@ -73,7 +74,7 @@ export class VisualBlock { parent.style.left = this.parent.left + "px"; parent.style.top = this.parent.top + "px"; parent.style.height = this.parent.height + "px"; - parent.style.width = `calc(100% - ${this.parent.left}px)`; + parent.style.width = this.width - this.parent.left + "px"; } // Optionally allows nested compound statements some breathing space @@ -82,7 +83,7 @@ export class VisualBlock { body.style.left = this.body.left - bodyPullBack + "px"; body.style.top = this.body.top + "px"; body.style.height = this.body.height + "px"; - body.style.width = `calc(100% - ${this.body.left - bodyPullBack}px)`; + body.style.width = this.width - this.body.left + bodyPullBack + "px"; } if (this.parent && parent && this.body && body && indent) { @@ -95,7 +96,11 @@ export class VisualBlock { } eq(other: VisualBlock) { - return equals(this.body, other.body) && equals(this.parent, other.parent); + return ( + equals(this.body, other.body) && + equals(this.parent, other.parent) && + this.width === other.width + ); } }