Add experimental support for updating markdown links on copy/paste (#209319)

* Add experimental support for updating markdown links on copy/paste

For #209318

* Remove log
This commit is contained in:
Matt Bierner 2024-04-02 08:15:12 -07:00 committed by GitHub
parent 8ef2d1d6a5
commit 998047ca2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 164 additions and 13 deletions

View File

@ -708,6 +708,15 @@
"%configuration.markdown.preferredMdPathExtensionStyle.includeExtension%",
"%configuration.markdown.preferredMdPathExtensionStyle.removeExtension%"
]
},
"markdown.experimental.updateLinksOnPaste": {
"type": "boolean",
"default": false,
"markdownDescription": "%configuration.markdown.experimental.updateLinksOnPaste%",
"scope": "resource",
"tags": [
"experimental"
]
}
}
},

View File

@ -91,5 +91,6 @@
"configuration.markdown.preferredMdPathExtensionStyle.removeExtension": "Prefer removing the file extension. For example, path completions to a file named `file.md` will insert `file` without the `.md`.",
"configuration.markdown.editor.filePaste.videoSnippet": "Snippet used when adding videos to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the video file.\n- `${title}` — The title used for the video. A snippet placeholder will automatically be created for this variable.",
"configuration.markdown.editor.filePaste.audioSnippet": "Snippet used when adding audio to Markdown. This snippet can use the following variables:\n- `${src}` — The resolved path of the audio file.\n- `${title}` — The title used for the audio. A snippet placeholder will automatically be created for this variable.",
"configuration.markdown.experimental.updateLinksOnPaste": "Enable/disable automatic updating of links in text that is copied and pasted from one Markdown editor to another.",
"workspaceTrust": "Required for loading styles configured in the workspace."
}

View File

@ -1,7 +1,7 @@
{
"name": "vscode-markdown-languageserver",
"description": "Markdown language server",
"version": "0.4.0",
"version": "0.5.0-alpha.3",
"author": "Microsoft Corporation",
"license": "MIT",
"engines": {
@ -18,7 +18,7 @@
"vscode-languageserver": "^8.1.0",
"vscode-languageserver-textdocument": "^1.0.8",
"vscode-languageserver-types": "^3.17.3",
"vscode-markdown-languageservice": "^0.5.0-alpha.1",
"vscode-markdown-languageservice": "^0.5.0-alpha.3",
"vscode-uri": "^3.0.7"
},
"devDependencies": {

View File

@ -24,6 +24,9 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const getEditForFileRenames = new RequestType<FileRename[], { participatingRenames: readonly FileRename[]; edit: lsp.WorkspaceEdit }, any>('markdown/getEditForFileRenames');
export const prepareUpdatePastedLinks = new RequestType<{ uri: string; ranges: lsp.Range[] }, string, any>('markdown/prepareUpdatePastedLinks');
export const getUpdatePastedLinksEdit = new RequestType<{ pasteIntoDoc: string; metadata: string; edits: lsp.TextEdit[] }, lsp.TextEdit[] | undefined, any>('markdown/getUpdatePastedLinksEdit');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, md.ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget');

View File

@ -262,6 +262,26 @@ export async function startServer(connection: Connection, serverConfig: {
};
}));
connection.onRequest(protocol.prepareUpdatePastedLinks, (async (params, token: CancellationToken) => {
const document = documents.get(params.uri);
if (!document) {
return undefined;
}
return mdLs!.prepareUpdatePastedLinks(document, params.ranges, token);
}));
connection.onRequest(protocol.getUpdatePastedLinksEdit, (async (params, token: CancellationToken) => {
const document = documents.get(params.pasteIntoDoc);
if (!document) {
return undefined;
}
// TODO: Figure out why range types are lying
const edits = params.edits.map((edit: any) => lsp.TextEdit.replace(lsp.Range.create(edit.range[0].line, edit.range[0].character, edit.range[1].line, edit.range[1].character), edit.newText));
return mdLs!.getUpdatePastedLinksEdit(document, edits, params.metadata, token);
}));
connection.onRequest(protocol.resolveLinkTarget, (async (params, token: CancellationToken) => {
return mdLs!.resolveLinkTarget(params.linkText, URI.parse(params.uri), token);
}));

View File

@ -63,6 +63,18 @@ class VsCodeDocument implements md.ITextDocument {
throw new Error('Document has been closed');
}
offsetAt(position: Position): number {
if (this.inMemoryDoc) {
return this.inMemoryDoc.offsetAt(position);
}
if (this.onDiskDoc) {
return this.onDiskDoc.offsetAt(position);
}
throw new Error('Document has been closed');
}
hasInMemoryDoc(): boolean {
return !!this.inMemoryDoc;
}

View File

@ -124,6 +124,11 @@ vscode-languageserver-protocol@^3.17.1:
vscode-jsonrpc "8.2.0"
vscode-languageserver-types "3.17.5"
vscode-languageserver-textdocument@^1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.11.tgz#0822a000e7d4dc083312580d7575fe9e3ba2e2bf"
integrity sha512-X+8T3GoiwTVlJbicx/sIAF+yuJAqz8VvwJyoMVhwEMoEKE/fkDmrqUgDMyBECcM2A2frVZIUj5HI/ErRXCfOeA==
vscode-languageserver-textdocument@^1.0.8:
version "1.0.8"
resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0"
@ -146,15 +151,16 @@ vscode-languageserver@^8.1.0:
dependencies:
vscode-languageserver-protocol "3.17.3"
vscode-markdown-languageservice@^0.5.0-alpha.1:
version "0.5.0-alpha.1"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.1.tgz#0b03f1f8e853e20352587a8de6a2f10f8d14c81c"
integrity sha512-7uVtRSr4+/xlsml9QtBkBAHPmtBv71CKEj6zhPTERYZEOHCwFsue1EHkESWBOGuqQ1NSLXPnHWENcDh5VD+bNw==
vscode-markdown-languageservice@^0.5.0-alpha.3:
version "0.5.0-alpha.3"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.5.0-alpha.3.tgz#417e853c73ce2e1a2460e52d8868576365d6cb05"
integrity sha512-wD9LO4CWtrp7dqbQBoQ6HbQpDpN2lUTC6SvDDjhZVDqRU0XqJP5YyO4FsvY+MWwz75b3QwapYYv4635EY4xhrA==
dependencies:
"@vscode/l10n" "^0.0.10"
node-html-parser "^6.1.5"
picomatch "^2.3.1"
vscode-languageserver-protocol "^3.17.1"
vscode-languageserver-textdocument "^1.0.11"
vscode-uri "^3.0.7"
vscode-uri@^3.0.7:

View File

@ -4,14 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType } from 'vscode-languageclient';
import { BaseLanguageClient, LanguageClientOptions, NotebookDocumentSyncRegistrationType, Range, TextEdit } from 'vscode-languageclient';
import { IMdParser } from '../markdownEngine';
import * as proto from './protocol';
import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file';
import { VsCodeMdWorkspace } from './workspace';
import { FileWatcherManager } from './fileWatchingManager';
import { IDisposable } from '../util/dispose';
import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file';
import { FileWatcherManager } from './fileWatchingManager';
import * as proto from './protocol';
import { VsCodeMdWorkspace } from './workspace';
export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;
@ -38,6 +37,21 @@ export class MdLanguageClient implements IDisposable {
getReferencesToFileInWorkspace(resource: vscode.Uri, token: vscode.CancellationToken) {
return this._client.sendRequest(proto.getReferencesToFileInWorkspace, { uri: resource.toString() }, token);
}
prepareUpdatePastedLinks(doc: vscode.Uri, ranges: readonly vscode.Range[], token: vscode.CancellationToken) {
return this._client.sendRequest(proto.prepareUpdatePastedLinks, {
uri: doc.toString(),
ranges: ranges.map(range => Range.create(range.start.line, range.start.character, range.end.line, range.end.character)),
}, token);
}
getUpdatePastedLinksEdit(pastingIntoDoc: vscode.Uri, edits: readonly vscode.TextEdit[], metadata: string, token: vscode.CancellationToken) {
return this._client.sendRequest(proto.getUpdatePastedLinksEdit, {
metadata,
pasteIntoDoc: pastingIntoDoc.toString(),
edits: edits.map(edit => TextEdit.replace(edit.range, edit.newText)),
}, token);
}
}
export async function startClient(factory: LanguageClientConstructor, parser: IMdParser): Promise<MdLanguageClient> {

View File

@ -32,6 +32,9 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const getEditForFileRenames = new RequestType<Array<FileRename>, { participatingRenames: readonly FileRename[]; edit: lsp.WorkspaceEdit }, any>('markdown/getEditForFileRenames');
export const prepareUpdatePastedLinks = new RequestType<{ uri: string; ranges: lsp.Range[] }, string, any>('markdown/prepareUpdatePastedLinks');
export const getUpdatePastedLinksEdit = new RequestType<{ pasteIntoDoc: string; metadata: string; edits: lsp.TextEdit[] }, lsp.TextEdit[] | undefined, any>('markdown/getUpdatePastedLinksEdit');
export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
export const resolveLinkTarget = new RequestType<{ linkText: string; uri: string }, ResolvedDocumentLinkTarget, any>('markdown/resolveLinkTarget');

View File

@ -20,6 +20,7 @@ import { MarkdownPreviewManager } from './preview/previewManager';
import { ExtensionContentSecurityPolicyArbiter } from './preview/security';
import { loadDefaultTelemetryReporter } from './telemetryReporter';
import { MdLinkOpener } from './util/openDocumentLink';
import { registerUpdatePastedLinks } from './languageFeatures/updateLinksOnPaste';
export function activateShared(
context: vscode.ExtensionContext,
@ -61,5 +62,6 @@ function registerMarkdownLanguageFeatures(
registerResourceDropOrPasteSupport(selector, parser),
registerPasteUrlSupport(selector, parser),
registerUpdateLinksOnRename(client),
registerUpdatePastedLinks(selector, client),
);
}

View File

@ -28,7 +28,7 @@ export class FindFileReferencesCommand implements Command {
location: vscode.ProgressLocation.Window,
title: vscode.l10n.t("Finding file references")
}, async (_progress, token) => {
const locations = (await this._client.getReferencesToFileInWorkspace(resource!, token)).map(loc => {
const locations = (await this._client.getReferencesToFileInWorkspace(resource, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range));
});

View File

@ -0,0 +1,81 @@
/*---------------------------------------------------------------------------------------------
* 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 { MdLanguageClient } from '../client/client';
import { Mime } from '../util/mimes';
class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider {
public static readonly kind = vscode.DocumentPasteEditKind.Empty.append('text', 'markdown', 'updateLinks');
public static readonly metadataMime = 'vnd.vscode.markdown.updateLinksMetadata';
constructor(
private readonly _client: MdLanguageClient,
) { }
async prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<void> {
if (!this._isEnabled(document)) {
return;
}
const metadata = await this._client.prepareUpdatePastedLinks(document.uri, ranges, token);
if (token.isCancellationRequested) {
return;
}
dataTransfer.set(UpdatePastedLinksEditProvider.metadataMime, new vscode.DataTransferItem(metadata));
}
async provideDocumentPasteEdits(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
_context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit[] | undefined> {
if (!this._isEnabled(document)) {
return;
}
const metadata = dataTransfer.get(UpdatePastedLinksEditProvider.metadataMime)?.value;
if (!metadata) {
return;
}
const textItem = dataTransfer.get(Mime.textPlain);
const text = await textItem?.asString();
if (!text || token.isCancellationRequested) {
return;
}
// TODO: Handle cases such as:
// - copy empty line
// - Copy with multiple cursors and paste into multiple locations
// - ...
const edits = await this._client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token);
if (!edits || !edits.length || token.isCancellationRequested) {
return;
}
const pasteEdit = new vscode.DocumentPasteEdit('', vscode.l10n.t("Paste and update pasted links"), UpdatePastedLinksEditProvider.kind);
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.set(document.uri, edits.map(x => new vscode.TextEdit(new vscode.Range(x.range.start.line, x.range.start.character, x.range.end.line, x.range.end.character,), x.newText)));
pasteEdit.additionalEdit = workspaceEdit;
return [pasteEdit];
}
private _isEnabled(document: vscode.TextDocument): boolean {
return vscode.workspace.getConfiguration('markdown', document.uri).get<boolean>('experimental.updateLinksOnPaste', false);
}
}
export function registerUpdatePastedLinks(selector: vscode.DocumentSelector, client: MdLanguageClient) {
return vscode.languages.registerDocumentPasteEditProvider(selector, new UpdatePastedLinksEditProvider(client), {
copyMimeTypes: [UpdatePastedLinksEditProvider.metadataMime],
providedPasteEditKinds: [UpdatePastedLinksEditProvider.kind],
pasteMimeTypes: [UpdatePastedLinksEditProvider.metadataMime],
});
}