Skip to content

Commit 962c0a0

Browse files
committed
Use a broadcast channel for md preview diff scroll sync
This makes scroll sync faster by using a broadcast channel instead of the normal vscode webview api. This means the two webviews can communicate with each other directly instead of having to go through the extension host and renderer each time
1 parent 3632569 commit 962c0a0

8 files changed

Lines changed: 136 additions & 88 deletions

File tree

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type { DiffScrollSyncData } from '../types/previewMessaging';
7+
8+
export class DiffScrollSyncManager {
9+
10+
readonly #previewId = Math.random().toString(36).slice(2, 10);
11+
readonly #onIncomingScroll: (line: number) => void;
12+
13+
#syncData: DiffScrollSyncData;
14+
readonly #channel: BroadcastChannel;
15+
#ignoreIncomingScrollUntil = 0;
16+
17+
constructor(sync: DiffScrollSyncData, onIncomingScroll: (line: number) => void) {
18+
this.#onIncomingScroll = onIncomingScroll;
19+
this.#syncData = sync;
20+
this.#channel = new BroadcastChannel(sync.channelName);
21+
this.#channel.onmessage = (event) => {
22+
const { line, sender } = event.data;
23+
if (sender === this.#previewId) {
24+
return;
25+
}
26+
if (typeof line !== 'number' || isNaN(line)) {
27+
return;
28+
}
29+
30+
const mappedLine = translateLineWithMappings(line, this.#syncData.lineMappings);
31+
this.#ignoreIncomingScrollUntil = Date.now() + 100;
32+
this.#onIncomingScroll(mappedLine);
33+
};
34+
}
35+
36+
update(sync: DiffScrollSyncData): void {
37+
this.#syncData = sync;
38+
}
39+
40+
broadcastScroll(line: number): void {
41+
if (!this.#channel || Date.now() < this.#ignoreIncomingScrollUntil) {
42+
return;
43+
}
44+
this.#channel.postMessage({ line, sender: this.#previewId });
45+
}
46+
}
47+
48+
function translateLineWithMappings(line: number, mappings: readonly number[] | undefined): number {
49+
if (!mappings?.length) {
50+
return line;
51+
}
52+
const sourceLine = Math.floor(line);
53+
const progress = line - sourceLine;
54+
const mappedLine = mappings[sourceLine] ?? line;
55+
if (progress <= 0) {
56+
return Math.max(0, mappedLine);
57+
}
58+
const nextMappedLine = mappings[sourceLine + 1];
59+
if (typeof nextMappedLine !== 'number') {
60+
return Math.max(0, mappedLine + progress);
61+
}
62+
return Math.max(0, mappedLine + ((nextMappedLine - mappedLine) * progress));
63+
}

‎extensions/markdown-language-features/preview-src/index.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import throttle = require('lodash.throttle');
1212
import morphdom from 'morphdom';
1313
import type { MarkdownPreviewLineChanges, ToWebviewMessage } from '../types/previewMessaging';
1414
import { isOfScheme, Schemes } from '../src/util/schemes';
15+
import { DiffScrollSyncManager } from './diffScrollSync';
1516

1617
let scrollDisabledCount = 0;
1718
let scrollDisabledTimer: number | undefined;
@@ -25,6 +26,18 @@ let lineChanges = settings.settings.lineChanges;
2526

2627
const vscode = acquireVsCodeApi();
2728

29+
const onDiffScroll = (mappedLine: number) => {
30+
scrollDisabledCount = 1;
31+
if (scrollDisabledTimer) {
32+
clearTimeout(scrollDisabledTimer);
33+
}
34+
scrollDisabledTimer = window.setTimeout(() => { scrollDisabledCount = 0; }, 100);
35+
doAfterImagesLoaded(() => scrollToRevealSourceLine(mappedLine, documentVersion, settings));
36+
};
37+
const diffScrollSyncManager = settings.settings.diffScrollSync
38+
? new DiffScrollSyncManager(settings.settings.diffScrollSync, onDiffScroll)
39+
: undefined;
40+
2841
interface State {
2942
scrollProgress?: number;
3043
resource?: string;
@@ -248,6 +261,9 @@ window.addEventListener('message', async event => {
248261

249262
case 'updateContent': {
250263
lineChanges = data.lineChanges;
264+
if (data.diffScrollSync) {
265+
diffScrollSyncManager?.update(data.diffScrollSync);
266+
}
251267
const root = document.querySelector('.markdown-body')!;
252268

253269
const parser = new DOMParser();
@@ -468,6 +484,7 @@ window.addEventListener('scroll', throttle(() => {
468484
state.line = line;
469485
vscode.setState(state);
470486
messaging.postMessage('revealLine', { line });
487+
diffScrollSyncManager?.broadcastScroll(line);
471488
}
472489
}, 50));
473490

‎extensions/markdown-language-features/preview-src/settings.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import type { MarkdownPreviewLineChanges } from '../types/previewMessaging';
6+
import type { DiffScrollSyncData, MarkdownPreviewLineChanges } from '../types/previewMessaging';
77

88
export interface PreviewSettings {
99
readonly source: string;
1010
readonly line?: number;
1111
readonly fragment?: string;
1212
readonly selectedLine?: number;
1313
readonly lineChanges?: MarkdownPreviewLineChanges;
14+
readonly diffScrollSync?: DiffScrollSyncData;
1415

1516
readonly scrollPreviewWithEditor?: boolean;
1617
readonly scrollEditorWithPreview: boolean;

‎extensions/markdown-language-features/src/preview/documentRenderer.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { WebviewResourceProvider } from '../util/resources';
1313
import { generateUuid } from '../util/uuid';
1414
import { MarkdownPreviewConfiguration, MarkdownPreviewConfigurationManager } from './previewConfig';
1515
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from './security';
16-
import type { MarkdownPreviewInnerChange, MarkdownPreviewLineChanges } from '../../types/previewMessaging';
16+
import type { DiffScrollSyncData, MarkdownPreviewInnerChange, MarkdownPreviewLineChanges } from '../../types/previewMessaging';
1717

1818

1919
/**
@@ -78,6 +78,7 @@ export class MdDocumentRenderer {
7878
state: any | undefined,
7979
imageInfo: readonly ImageInfo[],
8080
lineChanges: MarkdownPreviewLineChanges | undefined,
81+
diffScrollSync: DiffScrollSyncData | undefined,
8182
token: vscode.CancellationToken
8283
): Promise<MarkdownContentProviderOutput> {
8384
const sourceUri = markdownDocument.uri;
@@ -88,6 +89,7 @@ export class MdDocumentRenderer {
8889
line: initialLine,
8990
selectedLine,
9091
lineChanges,
92+
diffScrollSync,
9193
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
9294
scrollEditorWithPreview: config.scrollEditorWithPreview,
9395
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,

‎extensions/markdown-language-features/src/preview/lineDiff.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,14 @@ export class MarkdownPreviewLineDiffProvider {
5959
return translateLine(line, (await this.#getLineChanges()).modifiedToOriginal, this.#originalDocument.lineCount);
6060
}
6161

62+
public async getOriginalToModifiedMappings(): Promise<readonly number[]> {
63+
return (await this.#getLineChanges()).originalToModified;
64+
}
65+
66+
public async getModifiedToOriginalMappings(): Promise<readonly number[]> {
67+
return (await this.#getLineChanges()).modifiedToOriginal;
68+
}
69+
6270
#getLineChanges(): Promise<LineChanges> {
6371
if (!this.#cachedLineChanges || this.#cachedOriginalVersion !== this.#originalDocument.version || this.#cachedModifiedVersion !== this.#modifiedDocument.version) {
6472
this.#cachedOriginalVersion = this.#originalDocument.version;

‎extensions/markdown-language-features/src/preview/preview.ts‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ImageInfo, MdDocumentRenderer } from './documentRenderer';
1616
import { MarkdownPreviewConfigurationManager } from './previewConfig';
1717
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
1818
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from './topmostLineMonitor';
19-
import type { FromWebviewMessage, MarkdownPreviewLineChanges, ToWebviewMessage } from '../../types/previewMessaging';
19+
import type { DiffScrollSyncData, FromWebviewMessage, MarkdownPreviewLineChanges, ToWebviewMessage } from '../../types/previewMessaging';
2020

2121
export class PreviewDocumentVersion {
2222

@@ -38,6 +38,7 @@ interface MarkdownPreviewDelegate {
3838
getTitle?(resource: vscode.Uri): string;
3939
getAdditionalState(): {};
4040
getLineChanges?(): MarkdownPreviewLineChanges | Promise<MarkdownPreviewLineChanges | undefined> | undefined;
41+
getDiffScrollSync?(): DiffScrollSyncData | Promise<DiffScrollSyncData | undefined> | undefined;
4142
openPreviewLinkToMarkdownFile(markdownLink: vscode.Uri, fragment: string | undefined): void;
4243
}
4344

@@ -294,14 +295,15 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
294295
}
295296

296297
const lineChanges = await this.#delegate.getLineChanges?.();
298+
const diffScrollSync = await this.#delegate.getDiffScrollSync?.();
297299
const content = await (shouldReloadPage
298-
? this.#contentProvider.renderDocument(document, this, this.#previewConfigurations, this.#line, selectedLine, this.state, this.#imageInfo, lineChanges, this.#disposeCts.token)
300+
? this.#contentProvider.renderDocument(document, this, this.#previewConfigurations, this.#line, selectedLine, this.state, this.#imageInfo, lineChanges, diffScrollSync, this.#disposeCts.token)
299301
: this.#contentProvider.renderBody(document, this, lineChanges));
300302

301303
// Another call to `doUpdate` may have happened.
302304
// Make sure we are still updating for the correct document
303305
if (this.#currentVersion?.equals(pendingVersion)) {
304-
this.#updateWebviewContent(content.html, shouldReloadPage, lineChanges);
306+
this.#updateWebviewContent(content.html, shouldReloadPage, lineChanges, diffScrollSync);
305307
this.#updateImageWatchers(content.containingImages);
306308
}
307309
}
@@ -362,7 +364,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
362364
this.#webviewPanel.webview.html = this.#contentProvider.renderFileNotFoundDocument(this.#resource);
363365
}
364366

365-
#updateWebviewContent(html: string, reloadPage: boolean, lineChanges: MarkdownPreviewLineChanges | undefined): void {
367+
#updateWebviewContent(html: string, reloadPage: boolean, lineChanges: MarkdownPreviewLineChanges | undefined, diffScrollSync: DiffScrollSyncData | undefined): void {
366368
if (this.#disposed) {
367369
return;
368370
}
@@ -379,6 +381,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
379381
type: 'updateContent',
380382
content: html,
381383
lineChanges,
384+
diffScrollSync,
382385
source: this.#resource.toString(),
383386
});
384387
}
@@ -509,10 +512,11 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
509512
opener: MdLinkOpener,
510513
scrollLine?: number,
511514
getLineChanges?: () => MarkdownPreviewLineChanges | Promise<MarkdownPreviewLineChanges | undefined> | undefined,
515+
getDiffScrollSync?: () => DiffScrollSyncData | Promise<DiffScrollSyncData | undefined> | undefined,
512516
): StaticMarkdownPreview {
513517
webview.iconPath = contentProvider.iconPath;
514518

515-
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, opener, scrollLine, getLineChanges);
519+
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, opener, scrollLine, getLineChanges, getDiffScrollSync);
516520
}
517521

518522
readonly #preview: MarkdownPreview;
@@ -531,6 +535,7 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
531535
opener: MdLinkOpener,
532536
scrollLine?: number,
533537
getLineChanges?: () => MarkdownPreviewLineChanges | Promise<MarkdownPreviewLineChanges | undefined> | undefined,
538+
getDiffScrollSync?: () => DiffScrollSyncData | Promise<DiffScrollSyncData | undefined> | undefined,
534539
) {
535540
super();
536541

@@ -541,6 +546,7 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
541546
this.#preview = this._register(new MarkdownPreview(this.#webviewPanel, resource, topScrollLocation, {
542547
getAdditionalState: () => { return {}; },
543548
getLineChanges,
549+
getDiffScrollSync,
544550
openPreviewLinkToMarkdownFile: (markdownLink, fragment) => {
545551
return vscode.commands.executeCommand('vscode.openWith', markdownLink.with({
546552
fragment

‎extensions/markdown-language-features/src/preview/previewManager.ts‎

Lines changed: 22 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,14 @@ import { MarkdownContributionProvider } from '../markdownExtensions';
99
import { Disposable, disposeAll } from '../util/dispose';
1010
import { isMarkdownFile } from '../util/file';
1111
import { MdLinkOpener } from '../util/openDocumentLink';
12+
import { generateUuid } from '../util/uuid';
1213
import { MdDocumentRenderer } from './documentRenderer';
1314
import { MarkdownPreviewLineDiffProvider } from './lineDiff';
1415
import { DynamicMarkdownPreview, IManagedMarkdownPreview, StaticMarkdownPreview } from './preview';
1516
import { MarkdownPreviewConfigurationManager } from './previewConfig';
1617
import { scrollEditorToLine, StartingScrollFragment, StartingScrollLine, StartingScrollLocation } from './scrolling';
1718
import { getVisibleLine, TopmostLineMonitor } from './topmostLineMonitor';
18-
import type { MarkdownPreviewLineChanges } from '../../types/previewMessaging';
19+
import type { DiffScrollSyncData, MarkdownPreviewLineChanges } from '../../types/previewMessaging';
1920

2021

2122
export interface DynamicPreviewSettings {
@@ -267,17 +268,32 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
267268
webviewPanels: vscode.CustomEditorDiffWebviewPanels
268269
): Promise<void> {
269270
const lineDiffProvider = new MarkdownPreviewLineDiffProvider(documents.original, documents.modified);
270-
const originalPreview = this.#resolveCustomTextEditor(documents.original, webviewPanels.original, () => lineDiffProvider.getOriginalLineChanges());
271-
const modifiedPreview = this.#resolveCustomTextEditor(documents.modified, webviewPanels.modified, () => lineDiffProvider.getModifiedLineChanges());
271+
const channelName = `md-diff-scroll-${generateUuid()}`;
272+
const originalPreview = this.#resolveCustomTextEditor(
273+
documents.original, webviewPanels.original,
274+
() => lineDiffProvider.getOriginalLineChanges(),
275+
async () => ({
276+
channelName,
277+
role: 'original' as const,
278+
lineMappings: [...await lineDiffProvider.getModifiedToOriginalMappings()],
279+
}));
280+
const modifiedPreview = this.#resolveCustomTextEditor(
281+
documents.modified, webviewPanels.modified,
282+
() => lineDiffProvider.getModifiedLineChanges(),
283+
async () => ({
284+
channelName,
285+
role: 'modified' as const,
286+
lineMappings: [...await lineDiffProvider.getOriginalToModifiedMappings()],
287+
}));
272288
this.#refreshPreviewWhenDocumentChanges(originalPreview, documents.modified);
273289
this.#refreshPreviewWhenDocumentChanges(modifiedPreview, documents.original);
274-
this.#syncSideBySidePreviewScrolling(originalPreview, modifiedPreview, lineDiffProvider);
275290
}
276291

277292
#resolveCustomTextEditor(
278293
document: vscode.TextDocument,
279294
webview: vscode.WebviewPanel,
280295
getLineChanges?: () => MarkdownPreviewLineChanges | Promise<MarkdownPreviewLineChanges | undefined> | undefined,
296+
getDiffScrollSync?: () => DiffScrollSyncData | Promise<DiffScrollSyncData | undefined> | undefined,
281297
): StaticMarkdownPreview {
282298
const lineNumber = this.#topmostLineMonitor.getPreviousTextEditorLineByUri(document.uri);
283299
const preview = StaticMarkdownPreview.revive(
@@ -290,7 +306,8 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
290306
this.#contributions,
291307
this.#opener,
292308
lineNumber,
293-
getLineChanges
309+
getLineChanges,
310+
getDiffScrollSync
294311
);
295312
this.#registerStaticPreview(preview);
296313
this.#activePreview = preview;
@@ -306,82 +323,6 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
306323
preview.onDispose(() => listener.dispose());
307324
}
308325

309-
#syncSideBySidePreviewScrolling(originalPreview: StaticMarkdownPreview, modifiedPreview: StaticMarkdownPreview, lineDiffProvider: MarkdownPreviewLineDiffProvider): void {
310-
let disposed = false;
311-
let ignoreOriginalScrollUntil = 0;
312-
let ignoreModifiedScrollUntil = 0;
313-
let originalToModifiedRequest = 0;
314-
let modifiedToOriginalRequest = 0;
315-
316-
const syncScroll = async (
317-
line: number,
318-
targetPreview: StaticMarkdownPreview,
319-
translateLine: (line: number) => Promise<number>,
320-
request: number,
321-
shouldApply: (request: number) => boolean,
322-
ignoreTargetScroll: () => void,
323-
) => {
324-
if (disposed) {
325-
return;
326-
}
327-
328-
let targetLine: number;
329-
try {
330-
targetLine = await translateLine(line);
331-
} catch {
332-
targetLine = line;
333-
}
334-
335-
if (!disposed && shouldApply(request)) {
336-
ignoreTargetScroll();
337-
targetPreview.scrollTo(targetLine);
338-
}
339-
};
340-
341-
const disposables = [
342-
originalPreview.onScroll(({ line }) => {
343-
if (Date.now() < ignoreOriginalScrollUntil) {
344-
return;
345-
}
346-
const request = ++originalToModifiedRequest;
347-
void syncScroll(
348-
line,
349-
modifiedPreview,
350-
value => lineDiffProvider.translateOriginalLineToModified(value),
351-
request,
352-
value => value === originalToModifiedRequest,
353-
() => { ignoreModifiedScrollUntil = Date.now() + 100; },
354-
);
355-
}),
356-
modifiedPreview.onScroll(({ line }) => {
357-
if (Date.now() < ignoreModifiedScrollUntil) {
358-
return;
359-
}
360-
const request = ++modifiedToOriginalRequest;
361-
void syncScroll(
362-
line,
363-
originalPreview,
364-
value => lineDiffProvider.translateModifiedLineToOriginal(value),
365-
request,
366-
value => value === modifiedToOriginalRequest,
367-
() => { ignoreOriginalScrollUntil = Date.now() + 100; },
368-
);
369-
}),
370-
];
371-
372-
const dispose = () => {
373-
if (disposed) {
374-
return;
375-
}
376-
disposed = true;
377-
++originalToModifiedRequest;
378-
++modifiedToOriginalRequest;
379-
disposeAll(disposables);
380-
};
381-
originalPreview.onDispose(dispose);
382-
modifiedPreview.onDispose(dispose);
383-
}
384-
385326
#createNewDynamicPreview(
386327
resource: vscode.Uri,
387328
previewSettings: DynamicPreviewSettings,

0 commit comments

Comments
 (0)