diff --git a/extensions/markdown-language-features/src/features/preview.ts b/extensions/markdown-language-features/src/features/preview.ts index 4ecef4ac7b699..d9bcd0bb00d17 100644 --- a/extensions/markdown-language-features/src/features/preview.ts +++ b/extensions/markdown-language-features/src/features/preview.ts @@ -161,6 +161,10 @@ export class MarkdownPreview extends Disposable { this._onDidChangeViewStateEmitter.fire(e); }, null, this._disposables); + _contributionProvider.onContributionsChanged(() => { + setImmediate(() => this.refresh()); + }, null, this._disposables); + this.editor.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => { if (e.source !== this._resource.toString()) { return; diff --git a/extensions/markdown-language-features/src/markdownEngine.ts b/extensions/markdown-language-features/src/markdownEngine.ts index 090c8ab0da464..c26eb4834b82e 100644 --- a/extensions/markdown-language-features/src/markdownEngine.ts +++ b/extensions/markdown-language-features/src/markdownEngine.ts @@ -59,14 +59,19 @@ export class MarkdownEngine { public constructor( private readonly contributionProvider: MarkdownContributionProvider, private readonly slugifier: Slugifier, - ) { } + ) { + contributionProvider.onContributionsChanged(() => { + // Markdown plugin contributions may have changed + this.md = undefined; + }); + } private async getEngine(config: MarkdownItConfig): Promise { if (!this.md) { this.md = import('markdown-it').then(async markdownIt => { let md: MarkdownIt = markdownIt(await getMarkdownOptions(() => md)); - for (const plugin of this.contributionProvider.contributions.markdownItPlugins) { + for (const plugin of this.contributionProvider.contributions.markdownItPlugins.values()) { try { md = (await plugin)(md); } catch { diff --git a/extensions/markdown-language-features/src/markdownExtensions.ts b/extensions/markdown-language-features/src/markdownExtensions.ts index 4ca972f5c701b..567ee946b9d17 100644 --- a/extensions/markdown-language-features/src/markdownExtensions.ts +++ b/extensions/markdown-language-features/src/markdownExtensions.ts @@ -5,6 +5,8 @@ import * as vscode from 'vscode'; import * as path from 'path'; +import { Disposable } from './util/dispose'; +import * as arrays from './util/arrays'; const resolveExtensionResource = (extension: vscode.Extension, resourcePath: string): vscode.Uri => { return vscode.Uri.file(path.join(extension.extensionPath, resourcePath)) @@ -29,7 +31,7 @@ export interface MarkdownContributions { readonly previewScripts: ReadonlyArray; readonly previewStyles: ReadonlyArray; readonly previewResourceRoots: ReadonlyArray; - readonly markdownItPlugins: ReadonlyArray any>>; + readonly markdownItPlugins: Map any>>; } export namespace MarkdownContributions { @@ -37,7 +39,7 @@ export namespace MarkdownContributions { previewScripts: [], previewStyles: [], previewResourceRoots: [], - markdownItPlugins: [] + markdownItPlugins: new Map() }; export function merge(a: MarkdownContributions, b: MarkdownContributions): MarkdownContributions { @@ -45,10 +47,21 @@ export namespace MarkdownContributions { previewScripts: [...a.previewScripts, ...b.previewScripts], previewStyles: [...a.previewStyles, ...b.previewStyles], previewResourceRoots: [...a.previewResourceRoots, ...b.previewResourceRoots], - markdownItPlugins: [...a.markdownItPlugins, ...b.markdownItPlugins], + markdownItPlugins: new Map([...a.markdownItPlugins.entries(), ...b.markdownItPlugins.entries()]), }; } + function uriEqual(a: vscode.Uri, b: vscode.Uri): boolean { + return a.toString() === b.toString(); + } + + export function equal(a: MarkdownContributions, b: MarkdownContributions): boolean { + return arrays.equals(a.previewScripts, b.previewScripts, uriEqual) + && arrays.equals(a.previewStyles, b.previewStyles, uriEqual) + && arrays.equals(a.previewResourceRoots, b.previewResourceRoots, uriEqual) + && arrays.equals(Array.from(a.markdownItPlugins.keys()), Array.from(b.markdownItPlugins.keys())); + } + export function fromExtension( extension: vscode.Extension ): MarkdownContributions { @@ -69,7 +82,7 @@ export namespace MarkdownContributions { previewScripts: scripts, previewStyles: styles, previewResourceRoots, - markdownItPlugins: plugins ? [plugins] : [] + markdownItPlugins: plugins ? new Map([[extension.id, plugins]]) : new Map() }; } @@ -106,23 +119,42 @@ export namespace MarkdownContributions { export interface MarkdownContributionProvider { readonly extensionPath: string; readonly contributions: MarkdownContributions; + readonly onContributionsChanged: vscode.Event; } -class VSCodeExtensionMarkdownContributionProvider implements MarkdownContributionProvider { +class VSCodeExtensionMarkdownContributionProvider extends Disposable implements MarkdownContributionProvider { private _contributions?: MarkdownContributions; public constructor( public readonly extensionPath: string, - ) { } + ) { + super(); + + vscode.extensions.onDidChange(() => { + const currentContributions = this.getCurrentContributions(); + const existingContributions = this._contributions || MarkdownContributions.Empty; + if (!MarkdownContributions.equal(existingContributions, currentContributions)) { + this._contributions = currentContributions; + this._onContributionsChanged.fire(this); + } + }, undefined, this._disposables); + } + + private readonly _onContributionsChanged = new vscode.EventEmitter(); + public readonly onContributionsChanged = this._onContributionsChanged.event; public get contributions(): MarkdownContributions { if (!this._contributions) { - this._contributions = vscode.extensions.all.reduce( - (contributions, extension) => MarkdownContributions.merge(contributions, MarkdownContributions.fromExtension(extension)), - MarkdownContributions.Empty); + this._contributions = this.getCurrentContributions(); } return this._contributions; } + + private getCurrentContributions(): MarkdownContributions { + return vscode.extensions.all + .map(MarkdownContributions.fromExtension) + .reduce(MarkdownContributions.merge, MarkdownContributions.Empty); + } } export function getMarkdownExtensionContributions(context: vscode.ExtensionContext): MarkdownContributionProvider { diff --git a/extensions/markdown-language-features/src/test/engine.ts b/extensions/markdown-language-features/src/test/engine.ts index 1a674c8a50900..fef334b4cf54b 100644 --- a/extensions/markdown-language-features/src/test/engine.ts +++ b/extensions/markdown-language-features/src/test/engine.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as vscode from 'vscode'; import { MarkdownEngine } from '../markdownEngine'; import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions'; import { githubSlugifier } from '../slugify'; @@ -10,6 +11,7 @@ import { githubSlugifier } from '../slugify'; const emptyContributions = new class implements MarkdownContributionProvider { readonly extensionPath = ''; readonly contributions = MarkdownContributions.Empty; + readonly onContributionsChanged = new vscode.EventEmitter().event; }; export function createNewMarkdownEngine(): MarkdownEngine { diff --git a/extensions/markdown-language-features/src/util/arrays.ts b/extensions/markdown-language-features/src/util/arrays.ts new file mode 100644 index 0000000000000..10599259901a7 --- /dev/null +++ b/extensions/markdown-language-features/src/util/arrays.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean { + if (one.length !== other.length) { + return false; + } + + for (let i = 0, len = one.length; i < len; i++) { + if (!itemEquals(one[i], other[i])) { + return false; + } + } + + return true; +}