Skip to content

Commit 67cc096

Browse files
MeghanKulkarniMeghan Kulkarni
andauthored
allow images in markdown preview editor to be copied (microsoft#184432)
* allow images in markdown preview editor to be copied * resolved feedback * added findPreview method * removed copy image command from showPreview * clean up --------- Co-authored-by: Meghan Kulkarni <[email protected]>
1 parent 218c6d4 commit 67cc096

File tree

8 files changed

+120
-1
lines changed

8 files changed

+120
-1
lines changed

extensions/markdown-language-features/package.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,10 @@
121121
}
122122
],
123123
"commands": [
124+
{
125+
"command": "_markdown.copyImage",
126+
"title": "%markdown.copyImage.title%"
127+
},
124128
{
125129
"command": "markdown.showPreview",
126130
"title": "%markdown.preview.title%",
@@ -182,6 +186,12 @@
182186
}
183187
],
184188
"menus": {
189+
"webview/context": [
190+
{
191+
"command": "_markdown.copyImage",
192+
"when": "webviewId == 'markdown.preview' && webviewSection == 'image'"
193+
}
194+
],
185195
"editor/title": [
186196
{
187197
"command": "markdown.showPreviewToSide",

extensions/markdown-language-features/package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"displayName": "Markdown Language Features",
33
"description": "Provides rich language support for Markdown.",
4+
"markdown.copyImage.title": "Copy Image",
45
"markdown.preview.breaks.desc": "Sets how line-breaks are rendered in the Markdown preview. Setting it to 'true' creates a <br> for newlines inside paragraphs.",
56
"markdown.preview.linkify": "Convert URL-like text to links in the Markdown preview.",
67
"markdown.preview.typographer": "Enable some language-neutral replacement and quotes beautification in the Markdown preview.",

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ function doAfterImagesLoaded(cb: () => void) {
6363
onceDocumentLoaded(() => {
6464
const scrollProgress = state.scrollProgress;
6565

66+
addImageContexts();
6667
if (typeof scrollProgress === 'number' && !settings.settings.fragment) {
6768
doAfterImagesLoaded(() => {
6869
scrollDisabledCount += 1;
@@ -125,9 +126,58 @@ window.addEventListener('resize', () => {
125126
updateScrollProgress();
126127
}, true);
127128

129+
function addImageContexts() {
130+
const images = document.getElementsByTagName('img');
131+
let idNumber = 0;
132+
for (const img of images) {
133+
img.id = 'image-' + idNumber;
134+
idNumber += 1;
135+
img.setAttribute('data-vscode-context', JSON.stringify({ webviewSection: 'image', id: img.id, 'preventDefaultContextMenuItems': true, resource: documentResource }));
136+
}
137+
}
138+
139+
async function copyImage(image: HTMLImageElement, retries = 5) {
140+
if (!document.hasFocus() && retries > 0) {
141+
// copyImage is called at the same time as webview.reveal, which means this function is running whilst the webview is gaining focus.
142+
// Since navigator.clipboard.write requires the document to be focused, we need to wait for focus.
143+
// 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.
144+
setTimeout(() => { copyImage(image, retries - 1); }, 20);
145+
return;
146+
}
147+
148+
try {
149+
await navigator.clipboard.write([new ClipboardItem({
150+
'image/png': new Promise((resolve) => {
151+
const canvas = document.createElement('canvas');
152+
if (canvas !== null) {
153+
canvas.width = image.naturalWidth;
154+
canvas.height = image.naturalHeight;
155+
const context = canvas.getContext('2d');
156+
context?.drawImage(image, 0, 0);
157+
}
158+
canvas.toBlob((blob) => {
159+
if (blob) {
160+
resolve(blob);
161+
}
162+
canvas.remove();
163+
}, 'image/png');
164+
})
165+
})]);
166+
} catch (e) {
167+
console.error(e);
168+
}
169+
}
170+
128171
window.addEventListener('message', async event => {
129172
const data = event.data as ToWebviewMessage.Type;
130173
switch (data.type) {
174+
case 'copyImage': {
175+
const img = document.getElementById(data.id);
176+
if (img instanceof HTMLImageElement) {
177+
copyImage(img);
178+
}
179+
return;
180+
}
131181
case 'onDidChangeTextEditorSelection':
132182
if (data.source === documentResource) {
133183
marker.onDidChangeTextEditorSelection(data.line, documentVersion);
@@ -239,6 +289,7 @@ window.addEventListener('message', async event => {
239289
++documentVersion;
240290

241291
window.dispatchEvent(new CustomEvent('vscode.markdown.updateContent'));
292+
addImageContexts();
242293
break;
243294
}
244295
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 * as vscode from 'vscode';
7+
import { Command } from '../commandManager';
8+
import { MarkdownPreviewManager } from '../preview/previewManager';
9+
10+
export class CopyImageCommand implements Command {
11+
public readonly id = '_markdown.copyImage';
12+
13+
public constructor(
14+
private readonly _webviewManager: MarkdownPreviewManager,
15+
) { }
16+
17+
public execute(args: { id: string; resource: string }) {
18+
const source = vscode.Uri.parse(args.resource);
19+
this._webviewManager.findPreview(source)?.copyImage(args.id);
20+
}
21+
}

extensions/markdown-language-features/src/commands/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { RefreshPreviewCommand } from './refreshPreview';
1414
import { ReloadPlugins } from './reloadPlugins';
1515
import { RenderDocument } from './renderDocument';
1616
import { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
17+
import { CopyImageCommand } from './copyImage';
1718
import { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
1819
import { ShowSourceCommand } from './showSource';
1920
import { ToggleLockCommand } from './toggleLock';
@@ -27,6 +28,7 @@ export function registerMarkdownCommands(
2728
): vscode.Disposable {
2829
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
2930

31+
commandManager.register(new CopyImageCommand(previewManager));
3032
commandManager.register(new ShowPreviewCommand(previewManager, telemetryReporter));
3133
commandManager.register(new ShowPreviewToSideCommand(previewManager, telemetryReporter));
3234
commandManager.register(new ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -442,8 +442,8 @@ export interface IManagedMarkdownPreview {
442442
readonly onDispose: vscode.Event<void>;
443443
readonly onDidChangeViewState: vscode.Event<vscode.WebviewPanelOnDidChangeViewStateEvent>;
444444

445+
copyImage(id: string): void;
445446
dispose(): void;
446-
447447
refresh(): void;
448448
updateConfiguration(): void;
449449

@@ -515,6 +515,15 @@ export class StaticMarkdownPreview extends Disposable implements IManagedMarkdow
515515
}));
516516
}
517517

518+
copyImage(id: string) {
519+
this._webviewPanel.reveal();
520+
this._preview.postMessage({
521+
type: 'copyImage',
522+
source: this.resource.toString(),
523+
id: id
524+
});
525+
}
526+
518527
private readonly _onDispose = this._register(new vscode.EventEmitter<void>());
519528
public readonly onDispose = this._onDispose.event;
520529

@@ -661,6 +670,15 @@ export class DynamicMarkdownPreview extends Disposable implements IManagedMarkdo
661670
}));
662671
}
663672

673+
copyImage(id: string) {
674+
this._webviewPanel.reveal();
675+
this._preview.postMessage({
676+
type: 'copyImage',
677+
source: this.resource.toString(),
678+
id: id
679+
});
680+
}
681+
664682
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>());
665683
public readonly onDispose = this._onDisposeEmitter.event;
666684

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,15 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
147147
return this._activePreview?.resourceColumn;
148148
}
149149

150+
public findPreview(resource: vscode.Uri): IManagedMarkdownPreview | undefined {
151+
for (const preview of [...this._dynamicPreviews, ...this._staticPreviews]) {
152+
if (preview.resource.fsPath === resource.fsPath) {
153+
return preview;
154+
}
155+
}
156+
return undefined;
157+
}
158+
150159
public toggleLock() {
151160
const preview = this._activePreview;
152161
if (preview instanceof DynamicMarkdownPreview) {

extensions/markdown-language-features/types/previewMessaging.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,16 @@ export namespace ToWebviewMessage {
6565
readonly content: string;
6666
}
6767

68+
export interface CopyImageContent extends BaseMessage {
69+
readonly type: 'copyImage';
70+
readonly source: string;
71+
readonly id: string;
72+
}
73+
6874
export type Type =
6975
| OnDidChangeTextEditorSelection
7076
| UpdateView
7177
| UpdateContent
78+
| CopyImageContent
7279
;
7380
}

0 commit comments

Comments
 (0)