diff --git a/extensions/markdown-language-features/preview-src/scroll-sync.ts b/extensions/markdown-language-features/preview-src/scroll-sync.ts index 54921b4589479..a884730a15219 100644 --- a/extensions/markdown-language-features/preview-src/scroll-sync.ts +++ b/extensions/markdown-language-features/preview-src/scroll-sync.ts @@ -8,10 +8,20 @@ import { SettingsManager } from './settings'; const codeLineClass = 'code-line'; -export interface CodeLineElement { - element: HTMLElement; - line: number; - codeElement?: HTMLElement; +export class CodeLineElement { + private readonly _detailParentElements: readonly HTMLDetailsElement[]; + + constructor( + readonly element: HTMLElement, + readonly line: number, + readonly codeElement?: HTMLElement, + ) { + this._detailParentElements = Array.from(getParentsWithTagName(element, 'DETAILS')); + } + + get isVisible(): boolean { + return !this._detailParentElements.some(x => !x.open); + } } const getCodeLineElements = (() => { @@ -20,21 +30,26 @@ const getCodeLineElements = (() => { return (documentVersion: number) => { if (!cachedElements || documentVersion !== cachedVersion) { cachedVersion = documentVersion; - cachedElements = [{ element: document.body, line: -1 }]; + cachedElements = [new CodeLineElement(document.body, -1)]; for (const element of document.getElementsByClassName(codeLineClass)) { + if (!(element instanceof HTMLElement)) { + continue; + } + const line = +element.getAttribute('data-line')!; if (isNaN(line)) { continue; } + if (element.tagName === 'CODE' && element.parentElement && element.parentElement.tagName === 'PRE') { // Fenced code blocks are a special case since the `code-line` can only be marked on // the `` element and not the parent `
` element.
-					cachedElements.push({ element: element.parentElement as HTMLElement, line: line, codeElement: element as HTMLElement });
+					cachedElements.push(new CodeLineElement(element.parentElement, line, element));
 				} else if (element.tagName === 'UL' || element.tagName === 'OL') {
 					// Skip adding list elements since the first child has the same code line (and should be preferred)
 				} else {
-					cachedElements.push({ element: element as HTMLElement, line });
+					cachedElements.push(new CodeLineElement(element, line));
 				}
 			}
 		}
@@ -67,7 +82,7 @@ export function getElementsForSourceLine(targetLine: number, documentVersion: nu
  * Find the html elements that are at a specific pixel offset on the page.
  */
 export function getLineElementsAtPageOffset(offset: number, documentVersion: number): { previous: CodeLineElement; next?: CodeLineElement } {
-	const lines = getCodeLineElements(documentVersion);
+	const lines = getCodeLineElements(documentVersion).filter(x => x.isVisible);
 	const position = offset - window.scrollY;
 	let lo = -1;
 	let hi = lines.length - 1;
@@ -145,9 +160,12 @@ export function scrollToRevealSourceLine(line: number, documentVersion: number,
 	window.scroll(window.scrollX, Math.max(1, window.scrollY + scrollTo));
 }
 
-export function getEditorLineNumberForPageOffset(offset: number, documentVersion: number) {
+export function getEditorLineNumberForPageOffset(offset: number, documentVersion: number): number | null {
 	const { previous, next } = getLineElementsAtPageOffset(offset, documentVersion);
 	if (previous) {
+		if (previous.line < 0) {
+			return 0;
+		}
 		const previousBounds = getElementBounds(previous);
 		const offsetFromPrevious = (offset - window.scrollY - previousBounds.top);
 		if (next) {
@@ -169,3 +187,11 @@ export function getLineElementForFragment(fragment: string, documentVersion: num
 		return element.element.id === fragment;
 	});
 }
+
+function* getParentsWithTagName(element: HTMLElement, tagName: string): Iterable {
+	for (let parent = element.parentElement; parent; parent = parent.parentElement) {
+		if (parent.tagName === tagName) {
+			yield parent as T;
+		}
+	}
+}