diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json
index 8240a451ea9..bcac9df9e23 100644
--- a/extensions/markdown-language-features/package.json
+++ b/extensions/markdown-language-features/package.json
@@ -121,6 +121,10 @@
}
],
"commands": [
+ {
+ "command": "_markdown.copyImage",
+ "title": "%markdown.copyImage.title%"
+ },
{
"command": "markdown.showPreview",
"title": "%markdown.preview.title%",
@@ -182,6 +186,12 @@
}
],
"menus": {
+ "webview/context": [
+ {
+ "command": "_markdown.copyImage",
+ "when": "webviewId == 'markdown.preview' && webviewSection == 'image'"
+ }
+ ],
"editor/title": [
{
"command": "markdown.showPreviewToSide",
diff --git a/extensions/markdown-language-features/package.nls.json b/extensions/markdown-language-features/package.nls.json
index 8049ad94473..9f7d5f4bce6 100644
--- a/extensions/markdown-language-features/package.nls.json
+++ b/extensions/markdown-language-features/package.nls.json
@@ -1,6 +1,7 @@
{
"displayName": "Markdown Language Features",
"description": "Provides rich language support for Markdown.",
+ "markdown.copyImage.title": "Copy Image",
"markdown.preview.breaks.desc": "Sets how line-breaks are rendered in the Markdown preview. Setting it to 'true' creates a
for newlines inside paragraphs.",
"markdown.preview.linkify": "Convert URL-like text to links in the Markdown preview.",
"markdown.preview.typographer": "Enable some language-neutral replacement and quotes beautification in the Markdown preview.",
diff --git a/extensions/markdown-language-features/preview-src/index.ts b/extensions/markdown-language-features/preview-src/index.ts
index 4b012c5eeb1..bc922024b5b 100644
--- a/extensions/markdown-language-features/preview-src/index.ts
+++ b/extensions/markdown-language-features/preview-src/index.ts
@@ -63,6 +63,7 @@ function doAfterImagesLoaded(cb: () => void) {
onceDocumentLoaded(() => {
const scrollProgress = state.scrollProgress;
+ addImageContexts();
if (typeof scrollProgress === 'number' && !settings.settings.fragment) {
doAfterImagesLoaded(() => {
scrollDisabledCount += 1;
@@ -125,9 +126,58 @@ window.addEventListener('resize', () => {
updateScrollProgress();
}, true);
+function addImageContexts() {
+ const images = document.getElementsByTagName('img');
+ let idNumber = 0;
+ for (const img of images) {
+ img.id = 'image-' + idNumber;
+ idNumber += 1;
+ img.setAttribute('data-vscode-context', JSON.stringify({ webviewSection: 'image', id: img.id, 'preventDefaultContextMenuItems': true, resource: documentResource }));
+ }
+}
+
+async function copyImage(image: HTMLImageElement, retries = 5) {
+ if (!document.hasFocus() && retries > 0) {
+ // copyImage is called at the same time as webview.reveal, which means this function is running whilst the webview is gaining focus.
+ // Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.
+ // We cannot use a listener, as there is a high chance the focus is gained during the setup of the listener resulting in us missing it.
+ setTimeout(() => { copyImage(image, retries - 1); }, 20);
+ return;
+ }
+
+ try {
+ await navigator.clipboard.write([new ClipboardItem({
+ 'image/png': new Promise((resolve) => {
+ const canvas = document.createElement('canvas');
+ if (canvas !== null) {
+ canvas.width = image.naturalWidth;
+ canvas.height = image.naturalHeight;
+ const context = canvas.getContext('2d');
+ context?.drawImage(image, 0, 0);
+ }
+ canvas.toBlob((blob) => {
+ if (blob) {
+ resolve(blob);
+ }
+ canvas.remove();
+ }, 'image/png');
+ })
+ })]);
+ } catch (e) {
+ console.error(e);
+ }
+}
+
window.addEventListener('message', async event => {
const data = event.data as ToWebviewMessage.Type;
switch (data.type) {
+ case 'copyImage': {
+ const img = document.getElementById(data.id);
+ if (img instanceof HTMLImageElement) {
+ copyImage(img);
+ }
+ return;
+ }
case 'onDidChangeTextEditorSelection':
if (data.source === documentResource) {
marker.onDidChangeTextEditorSelection(data.line, documentVersion);
@@ -239,6 +289,7 @@ window.addEventListener('message', async event => {
++documentVersion;
window.dispatchEvent(new CustomEvent('vscode.markdown.updateContent'));
+ addImageContexts();
break;
}
}
diff --git a/extensions/markdown-language-features/src/commands/copyImage.ts b/extensions/markdown-language-features/src/commands/copyImage.ts
new file mode 100644
index 00000000000..86fd349c730
--- /dev/null
+++ b/extensions/markdown-language-features/src/commands/copyImage.ts
@@ -0,0 +1,21 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import * as vscode from 'vscode';
+import { Command } from '../commandManager';
+import { MarkdownPreviewManager } from '../preview/previewManager';
+
+export class CopyImageCommand implements Command {
+ public readonly id = '_markdown.copyImage';
+
+ public constructor(
+ private readonly _webviewManager: MarkdownPreviewManager,
+ ) { }
+
+ public execute(args: { id: string; resource: string }) {
+ const source = vscode.Uri.parse(args.resource);
+ this._webviewManager.findPreview(source)?.copyImage(args.id);
+ }
+}
diff --git a/extensions/markdown-language-features/src/commands/index.ts b/extensions/markdown-language-features/src/commands/index.ts
index 8904d4a547c..e1a4f2b41ff 100644
--- a/extensions/markdown-language-features/src/commands/index.ts
+++ b/extensions/markdown-language-features/src/commands/index.ts
@@ -14,6 +14,7 @@ import { RefreshPreviewCommand } from './refreshPreview';
import { ReloadPlugins } from './reloadPlugins';
import { RenderDocument } from './renderDocument';
import { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
+import { CopyImageCommand } from './copyImage';
import { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
import { ShowSourceCommand } from './showSource';
import { ToggleLockCommand } from './toggleLock';
@@ -27,6 +28,7 @@ export function registerMarkdownCommands(
): vscode.Disposable {
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
+ commandManager.register(new CopyImageCommand(previewManager));
commandManager.register(new ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
diff --git a/extensions/markdown-language-features/src/preview/preview.ts b/extensions/markdown-language-features/src/preview/preview.ts
index 828442a76e5..de58e54784a 100644
--- a/extensions/markdown-language-features/src/preview/preview.ts
+++ b/extensions/markdown-language-features/src/preview/preview.ts
@@ -442,8 +442,8 @@ export interface IManagedMarkdownPreview {
readonly onDispose: vscode.Event;
readonly onDidChangeViewState: vscode.Event;
+ copyImage(id: string): void;
dispose(): void;
-
refresh(): void;
updateConfiguration(): void;
@@ -515,6 +515,15 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
}));
}
+ copyImage(id: string) {
+ this._webviewPanel.reveal();
+ this._preview.postMessage({
+ type: 'copyImage',
+ source: this.resource.toString(),
+ id: id
+ });
+ }
+
private readonly _onDispose = this._register(new vscode.EventEmitter());
public readonly onDispose = this._onDispose.event;
@@ -661,6 +670,15 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
}));
}
+ copyImage(id: string) {
+ this._webviewPanel.reveal();
+ this._preview.postMessage({
+ type: 'copyImage',
+ source: this.resource.toString(),
+ id: id
+ });
+ }
+
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter());
public readonly onDispose = this._onDisposeEmitter.event;
diff --git a/extensions/markdown-language-features/src/preview/previewManager.ts b/extensions/markdown-language-features/src/preview/previewManager.ts
index 7f6de5023b3..3b46c2a4322 100644
--- a/extensions/markdown-language-features/src/preview/previewManager.ts
+++ b/extensions/markdown-language-features/src/preview/previewManager.ts
@@ -147,6 +147,15 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
return this._activePreview?.resourceColumn;
}
+ public findPreview(resource: vscode.Uri): IManagedMarkdownPreview | undefined {
+ for (const preview of [...this._dynamicPreviews, ...this._staticPreviews]) {
+ if (preview.resource.fsPath === resource.fsPath) {
+ return preview;
+ }
+ }
+ return undefined;
+ }
+
public toggleLock() {
const preview = this._activePreview;
if (preview instanceof DynamicMarkdownPreview) {
diff --git a/extensions/markdown-language-features/types/previewMessaging.d.ts b/extensions/markdown-language-features/types/previewMessaging.d.ts
index ee8f91f596f..05d10af6597 100644
--- a/extensions/markdown-language-features/types/previewMessaging.d.ts
+++ b/extensions/markdown-language-features/types/previewMessaging.d.ts
@@ -65,9 +65,16 @@ export namespace ToWebviewMessage {
readonly content: string;
}
+ export interface CopyImageContent extends BaseMessage {
+ readonly type: 'copyImage';
+ readonly source: string;
+ readonly id: string;
+ }
+
export type Type =
| OnDidChangeTextEditorSelection
| UpdateView
| UpdateContent
+ | CopyImageContent
;
}