mirror of
https://github.com/Microsoft/vscode
synced 2024-10-04 02:14:06 +00:00
allow images in markdown preview editor to be copied (#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 <t-mekulkarni@microsoft.com>
This commit is contained in:
parent
218c6d4f9b
commit
67cc0965b3
|
@ -121,6 +121,10 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"commands": [
|
"commands": [
|
||||||
|
{
|
||||||
|
"command": "_markdown.copyImage",
|
||||||
|
"title": "%markdown.copyImage.title%"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"command": "markdown.showPreview",
|
"command": "markdown.showPreview",
|
||||||
"title": "%markdown.preview.title%",
|
"title": "%markdown.preview.title%",
|
||||||
|
@ -182,6 +186,12 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"menus": {
|
"menus": {
|
||||||
|
"webview/context": [
|
||||||
|
{
|
||||||
|
"command": "_markdown.copyImage",
|
||||||
|
"when": "webviewId == 'markdown.preview' && webviewSection == 'image'"
|
||||||
|
}
|
||||||
|
],
|
||||||
"editor/title": [
|
"editor/title": [
|
||||||
{
|
{
|
||||||
"command": "markdown.showPreviewToSide",
|
"command": "markdown.showPreviewToSide",
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
{
|
{
|
||||||
"displayName": "Markdown Language Features",
|
"displayName": "Markdown Language Features",
|
||||||
"description": "Provides rich language support for Markdown.",
|
"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 <br> for newlines inside paragraphs.",
|
"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.",
|
||||||
"markdown.preview.linkify": "Convert URL-like text to links in the Markdown preview.",
|
"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.",
|
"markdown.preview.typographer": "Enable some language-neutral replacement and quotes beautification in the Markdown preview.",
|
||||||
|
|
|
@ -63,6 +63,7 @@ function doAfterImagesLoaded(cb: () => void) {
|
||||||
onceDocumentLoaded(() => {
|
onceDocumentLoaded(() => {
|
||||||
const scrollProgress = state.scrollProgress;
|
const scrollProgress = state.scrollProgress;
|
||||||
|
|
||||||
|
addImageContexts();
|
||||||
if (typeof scrollProgress === 'number' && !settings.settings.fragment) {
|
if (typeof scrollProgress === 'number' && !settings.settings.fragment) {
|
||||||
doAfterImagesLoaded(() => {
|
doAfterImagesLoaded(() => {
|
||||||
scrollDisabledCount += 1;
|
scrollDisabledCount += 1;
|
||||||
|
@ -125,9 +126,58 @@ window.addEventListener('resize', () => {
|
||||||
updateScrollProgress();
|
updateScrollProgress();
|
||||||
}, true);
|
}, 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 => {
|
window.addEventListener('message', async event => {
|
||||||
const data = event.data as ToWebviewMessage.Type;
|
const data = event.data as ToWebviewMessage.Type;
|
||||||
switch (data.type) {
|
switch (data.type) {
|
||||||
|
case 'copyImage': {
|
||||||
|
const img = document.getElementById(data.id);
|
||||||
|
if (img instanceof HTMLImageElement) {
|
||||||
|
copyImage(img);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
case 'onDidChangeTextEditorSelection':
|
case 'onDidChangeTextEditorSelection':
|
||||||
if (data.source === documentResource) {
|
if (data.source === documentResource) {
|
||||||
marker.onDidChangeTextEditorSelection(data.line, documentVersion);
|
marker.onDidChangeTextEditorSelection(data.line, documentVersion);
|
||||||
|
@ -239,6 +289,7 @@ window.addEventListener('message', async event => {
|
||||||
++documentVersion;
|
++documentVersion;
|
||||||
|
|
||||||
window.dispatchEvent(new CustomEvent('vscode.markdown.updateContent'));
|
window.dispatchEvent(new CustomEvent('vscode.markdown.updateContent'));
|
||||||
|
addImageContexts();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -14,6 +14,7 @@ import { RefreshPreviewCommand } from './refreshPreview';
|
||||||
import { ReloadPlugins } from './reloadPlugins';
|
import { ReloadPlugins } from './reloadPlugins';
|
||||||
import { RenderDocument } from './renderDocument';
|
import { RenderDocument } from './renderDocument';
|
||||||
import { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
|
import { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
|
||||||
|
import { CopyImageCommand } from './copyImage';
|
||||||
import { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
|
import { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
|
||||||
import { ShowSourceCommand } from './showSource';
|
import { ShowSourceCommand } from './showSource';
|
||||||
import { ToggleLockCommand } from './toggleLock';
|
import { ToggleLockCommand } from './toggleLock';
|
||||||
|
@ -27,6 +28,7 @@ export function registerMarkdownCommands(
|
||||||
): vscode.Disposable {
|
): vscode.Disposable {
|
||||||
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
|
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
|
||||||
|
|
||||||
|
commandManager.register(new CopyImageCommand(previewManager));
|
||||||
commandManager.register(new ShowPreviewCommand(previewManager, telemetryReporter));
|
commandManager.register(new ShowPreviewCommand(previewManager, telemetryReporter));
|
||||||
commandManager.register(new ShowPreviewToSideCommand(previewManager, telemetryReporter));
|
commandManager.register(new ShowPreviewToSideCommand(previewManager, telemetryReporter));
|
||||||
commandManager.register(new ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
|
commandManager.register(new ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
|
||||||
|
|
|
@ -442,8 +442,8 @@ export interface IManagedMarkdownPreview {
|
||||||
readonly onDispose: vscode.Event<void>;
|
readonly onDispose: vscode.Event<void>;
|
||||||
readonly onDidChangeViewState: vscode.Event<vscode.WebviewPanelOnDidChangeViewStateEvent>;
|
readonly onDidChangeViewState: vscode.Event<vscode.WebviewPanelOnDidChangeViewStateEvent>;
|
||||||
|
|
||||||
|
copyImage(id: string): void;
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
|
|
||||||
refresh(): void;
|
refresh(): void;
|
||||||
updateConfiguration(): 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<void>());
|
private readonly _onDispose = this._register(new vscode.EventEmitter<void>());
|
||||||
public readonly onDispose = this._onDispose.event;
|
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<void>());
|
private readonly _onDisposeEmitter = this._register(new vscode.EventEmitter<void>());
|
||||||
public readonly onDispose = this._onDisposeEmitter.event;
|
public readonly onDispose = this._onDisposeEmitter.event;
|
||||||
|
|
||||||
|
|
|
@ -147,6 +147,15 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||||
return this._activePreview?.resourceColumn;
|
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() {
|
public toggleLock() {
|
||||||
const preview = this._activePreview;
|
const preview = this._activePreview;
|
||||||
if (preview instanceof DynamicMarkdownPreview) {
|
if (preview instanceof DynamicMarkdownPreview) {
|
||||||
|
|
|
@ -65,9 +65,16 @@ export namespace ToWebviewMessage {
|
||||||
readonly content: string;
|
readonly content: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CopyImageContent extends BaseMessage {
|
||||||
|
readonly type: 'copyImage';
|
||||||
|
readonly source: string;
|
||||||
|
readonly id: string;
|
||||||
|
}
|
||||||
|
|
||||||
export type Type =
|
export type Type =
|
||||||
| OnDidChangeTextEditorSelection
|
| OnDidChangeTextEditorSelection
|
||||||
| UpdateView
|
| UpdateView
|
||||||
| UpdateContent
|
| UpdateContent
|
||||||
|
| CopyImageContent
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue