Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 2 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
}
},
Expand Down
1 change: 1 addition & 0 deletions extensions/markdown-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
4 changes: 2 additions & 2 deletions extensions/markdown-language-features/server/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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": {
Expand Down
3 changes: 3 additions & 0 deletions extensions/markdown-language-features/server/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
20 changes: 20 additions & 0 deletions extensions/markdown-language-features/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}));
Expand Down
12 changes: 12 additions & 0 deletions extensions/markdown-language-features/server/src/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
14 changes: 10 additions & 4 deletions extensions/markdown-language-features/server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
24 changes: 19 additions & 5 deletions extensions/markdown-language-features/src/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { IDisposable } from '../util/dispose';
import { looksLikeMarkdownPath, markdownFileExtensions } from '../util/file';
import { VsCodeMdWorkspace } from './workspace';
import { FileWatcherManager } from './fileWatchingManager';
import { IDisposable } from '../util/dispose';

import * as proto from './protocol';
import { VsCodeMdWorkspace } from './workspace';

export type LanguageClientConstructor = (name: string, description: string, clientOptions: LanguageClientOptions) => BaseLanguageClient;

Expand All @@ -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> {
Expand Down
3 changes: 3 additions & 0 deletions extensions/markdown-language-features/src/client/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
2 changes: 2 additions & 0 deletions extensions/markdown-language-features/src/extension.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -61,5 +62,6 @@ function registerMarkdownLanguageFeatures(
registerResourceDropOrPasteSupport(selector, parser),
registerPasteUrlSupport(selector, parser),
registerUpdateLinksOnRename(client),
registerUpdatePastedLinks(selector, client),
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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));
});

Expand Down
Original file line number Diff line number Diff line change
@@ -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],
});
}
Loading