Add commands to insert images/links in markdown (#163706)

* Add commands to insert images/links in markdown

Fixes #162809

* Rename commands and allow passing in uris

* Support selecting many images/files
This commit is contained in:
Matt Bierner 2022-10-14 16:05:36 -07:00 committed by GitHub
parent 129dbaa32b
commit 641046a11d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 186 additions and 49 deletions

View file

@ -30,6 +30,8 @@
"onCommand:markdown.api.render",
"onCommand:markdown.api.reloadPlugins",
"onCommand:markdown.findAllFileReferences",
"onCommand:markdown.editor.insertLinkFromWorkspace",
"onCommand:markdown.editor.insertImageFromWorkspace",
"onWebviewPanel:markdown.preview",
"onCustomEditor:vscode.markdown.preview.editor"
],
@ -175,6 +177,18 @@
"command": "markdown.findAllFileReferences",
"title": "%markdown.findAllFileReferences%",
"category": "Markdown"
},
{
"command": "markdown.editor.insertLinkFromWorkspace",
"title": "%markdown.editor.insertLinkFromWorkspace%",
"category": "Markdown",
"enablement": "editorLangId == markdown"
},
{
"command": "markdown.editor.insertImageFromWorkspace",
"title": "%markdown.editor.insertImageFromWorkspace%",
"category": "Markdown",
"enablement": "editorLangId == markdown"
}
],
"menus": {

View file

@ -22,6 +22,8 @@
"markdown.preview.refresh.title": "Refresh Preview",
"markdown.preview.toggleLock.title": "Toggle Preview Locking",
"markdown.findAllFileReferences": "Find File References",
"markdown.editor.insertLinkFromWorkspace": "Insert Link to File in Workspace",
"markdown.editor.insertImageFromWorkspace": "Insert Image from Workspace",
"configuration.markdown.preview.openMarkdownLinks.description": "Controls how links to other Markdown files in the Markdown preview should be opened.",
"configuration.markdown.preview.openMarkdownLinks.inEditor": "Try to open links in the editor.",
"configuration.markdown.preview.openMarkdownLinks.inPreview": "Try to open links in the Markdown preview.",

View file

@ -3,11 +3,41 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export { RefreshPreviewCommand } from './refreshPreview';
export { ReloadPlugins } from './reloadPlugins';
export { RenderDocument } from './renderDocument';
export { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
export { ShowSourceCommand } from './showSource';
export { ToggleLockCommand } from './toggleLock';
import * as vscode from 'vscode';
import { CommandManager } from '../commandManager';
import { MarkdownItEngine } from '../markdownEngine';
import { MarkdownPreviewManager } from '../preview/previewManager';
import { ContentSecurityPolicyArbiter, PreviewSecuritySelector } from '../preview/security';
import { TelemetryReporter } from '../telemetryReporter';
import { InsertLinkFromWorkspace, InsertImageFromWorkspace } from './insertResource';
import { RefreshPreviewCommand } from './refreshPreview';
import { ReloadPlugins } from './reloadPlugins';
import { RenderDocument } from './renderDocument';
import { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
import { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
import { ShowSourceCommand } from './showSource';
import { ToggleLockCommand } from './toggleLock';
export function registerMarkdownCommands(
commandManager: CommandManager,
previewManager: MarkdownPreviewManager,
telemetryReporter: TelemetryReporter,
cspArbiter: ContentSecurityPolicyArbiter,
engine: MarkdownItEngine,
): vscode.Disposable {
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
commandManager.register(new ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new ShowSourceCommand(previewManager));
commandManager.register(new RefreshPreviewCommand(previewManager, engine));
commandManager.register(new ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new ToggleLockCommand(previewManager));
commandManager.register(new RenderDocument(engine));
commandManager.register(new ReloadPlugins(previewManager, engine));
commandManager.register(new InsertLinkFromWorkspace());
commandManager.register(new InsertImageFromWorkspace());
return commandManager;
}

View file

@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* 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 * as nls from 'vscode-nls';
import { Command } from '../commandManager';
import { createUriListSnippet, getParentDocumentUri, imageFileExtensions } from '../languageFeatures/dropIntoEditor';
import { coalesce } from '../util/arrays';
const localize = nls.loadMessageBundle();
export class InsertLinkFromWorkspace implements Command {
public readonly id = 'markdown.editor.insertLinkFromWorkspace';
public async execute(resources?: vscode.Uri[]) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
resources ??= await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: true,
canSelectMany: true,
openLabel: localize('insertLink.openLabel', "Insert link"),
title: localize('insertLink.title', "Insert link"),
defaultUri: getParentDocumentUri(activeEditor.document),
});
return insertLink(activeEditor, resources ?? [], false);
}
}
export class InsertImageFromWorkspace implements Command {
public readonly id = 'markdown.editor.insertImageFromWorkspace';
public async execute(resources?: vscode.Uri[]) {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
return;
}
resources ??= await vscode.window.showOpenDialog({
canSelectFiles: true,
canSelectFolders: false,
canSelectMany: true,
filters: {
[localize('insertImage.imagesLabel', "Images")]: Array.from(imageFileExtensions)
},
openLabel: localize('insertImage.openLabel', "Insert image"),
title: localize('insertImage.title', "Insert image"),
defaultUri: getParentDocumentUri(activeEditor.document),
});
return insertLink(activeEditor, resources ?? [], true);
}
}
async function insertLink(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsImage: boolean): Promise<void> {
if (!selectedFiles.length) {
return;
}
const edit = createInsertLinkEdit(activeEditor, selectedFiles, insertAsImage);
await vscode.workspace.applyEdit(edit);
}
function createInsertLinkEdit(activeEditor: vscode.TextEditor, selectedFiles: vscode.Uri[], insertAsImage: boolean) {
const snippetEdits = coalesce(activeEditor.selections.map((selection, i): vscode.SnippetTextEdit | undefined => {
const selectionText = activeEditor.document.getText(selection);
const snippet = createUriListSnippet(activeEditor.document, selectedFiles, {
insertAsImage: insertAsImage,
placeholderText: selectionText,
placeholderStartIndex: (i + 1) * selectedFiles.length,
});
return snippet ? new vscode.SnippetTextEdit(selection, snippet) : undefined;
}));
const edit = new vscode.WorkspaceEdit();
edit.set(activeEditor.document.uri, snippetEdits);
return edit;
}

View file

@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { MdLanguageClient } from './client/client';
import { CommandManager } from './commandManager';
import * as commands from './commands/index';
import { registerMarkdownCommands } from './commands/index';
import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
@ -17,8 +17,8 @@ import { MarkdownItEngine } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
import { MdDocumentRenderer } from './preview/documentRenderer';
import { MarkdownPreviewManager } from './preview/previewManager';
import { ContentSecurityPolicyArbiter, ExtensionContentSecurityPolicyArbiter, PreviewSecuritySelector } from './preview/security';
import { loadDefaultTelemetryReporter, TelemetryReporter } from './telemetryReporter';
import { ExtensionContentSecurityPolicyArbiter } from './preview/security';
import { loadDefaultTelemetryReporter } from './telemetryReporter';
import { MdLinkOpener } from './util/openDocumentLink';
export function activateShared(
@ -63,23 +63,3 @@ function registerMarkdownLanguageFeatures(
);
}
function registerMarkdownCommands(
commandManager: CommandManager,
previewManager: MarkdownPreviewManager,
telemetryReporter: TelemetryReporter,
cspArbiter: ContentSecurityPolicyArbiter,
engine: MarkdownItEngine,
): vscode.Disposable {
const previewSecuritySelector = new PreviewSecuritySelector(cspArbiter, previewManager);
commandManager.register(new commands.ShowPreviewCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowLockedPreviewToSideCommand(previewManager, telemetryReporter));
commandManager.register(new commands.ShowSourceCommand(previewManager));
commandManager.register(new commands.RefreshPreviewCommand(previewManager, engine));
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new commands.ToggleLockCommand(previewManager));
commandManager.register(new commands.RenderDocument(engine));
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
return commandManager;
}

View file

@ -8,20 +8,20 @@ import * as vscode from 'vscode';
import * as URI from 'vscode-uri';
import { Schemes } from '../util/schemes';
const imageFileExtensions = new Set<string>([
'.bmp',
'.gif',
'.ico',
'.jpe',
'.jpeg',
'.jpg',
'.png',
'.psd',
'.svg',
'.tga',
'.tif',
'.tiff',
'.webp',
export const imageFileExtensions = new Set<string>([
'bmp',
'gif',
'ico',
'jpe',
'jpeg',
'jpg',
'png',
'psd',
'svg',
'tga',
'tif',
'tiff',
'webp',
]);
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
@ -56,7 +56,20 @@ export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTr
return createUriListSnippet(document, uris);
}
export function createUriListSnippet(document: vscode.TextDocument, uris: readonly vscode.Uri[]): vscode.SnippetString | undefined {
interface UriListSnippetOptions {
readonly placeholderText?: string;
readonly placeholderStartIndex?: number;
/**
* Should the snippet be for an image?
*
* If `undefined`, tries to infer this from the uri.
*/
readonly insertAsImage?: boolean;
}
export function createUriListSnippet(document: vscode.TextDocument, uris: readonly vscode.Uri[], options?: UriListSnippetOptions): vscode.SnippetString | undefined {
if (!uris.length) {
return undefined;
}
@ -69,9 +82,15 @@ export function createUriListSnippet(document: vscode.TextDocument, uris: readon
? encodeURI(path.relative(dir.fsPath, uri.fsPath).replace(/\\/g, '/'))
: uri.toString(false);
const ext = URI.Utils.extname(uri).toLowerCase();
snippet.appendText(imageFileExtensions.has(ext) ? '![' : '[');
snippet.appendTabstop();
const ext = URI.Utils.extname(uri).toLowerCase().replace('.', '');
const insertAsImage = typeof options?.insertAsImage === 'undefined' ? imageFileExtensions.has(ext) : !!options.insertAsImage;
snippet.appendText(insertAsImage ? '![' : '[');
const placeholderText = options?.placeholderText ?? (insertAsImage ? 'Alt text' : 'label');
const placeholderIndex = typeof options?.placeholderStartIndex !== 'undefined' ? options?.placeholderStartIndex + i : undefined;
snippet.appendPlaceholder(placeholderText, placeholderIndex);
snippet.appendText(`](${mdPath})`);
if (i <= uris.length - 1 && uris.length > 1) {
@ -89,7 +108,7 @@ function getDocumentDir(document: vscode.TextDocument): vscode.Uri | undefined {
return URI.Utils.dirname(docUri);
}
function getParentDocumentUri(document: vscode.TextDocument): vscode.Uri {
export function getParentDocumentUri(document: vscode.TextDocument): vscode.Uri {
if (document.uri.scheme === Schemes.notebookCell) {
for (const notebook of vscode.workspace.notebookDocuments) {
for (const cell of notebook.getCells()) {

View file

@ -2,6 +2,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/**
* @returns New array with all falsy values removed. The original array IS NOT modified.
*/
export function coalesce<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}
export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEquals: (a: T, b: T) => boolean = (a, b) => a === b): boolean {
if (one.length !== other.length) {