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 update markdown links on file moves/renames #157209

Merged
merged 2 commits into from
Aug 9, 2022
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
28 changes: 28 additions & 0 deletions extensions/markdown-language-features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,34 @@
"tags": [
"experimental"
]
},
"markdown.experimental.updateLinksOnFileMove.enabled": {
"type": "string",
"enum": [
"prompt",
"always",
"never"
],
"markdownEnumDescriptions": [
"%configuration.markdown.experimental.updateLinksOnFileMove.enabled.prompt%",
"%configuration.markdown.experimental.updateLinksOnFileMove.enabled.always%",
"%configuration.markdown.experimental.updateLinksOnFileMove.enabled.never%"
],
"default": "never",
"markdownDescription": "%configuration.markdown.experimental.updateLinksOnFileMove.enabled%",
"scope": "resource",
"tags": [
"experimental"
]
},
"markdown.experimental.updateLinksOnFileMove.externalFileGlobs": {
"type": "string",
"default": "**/*.{jpg,jpe,jpeg,png,bmp,gif,ico,webp,avif}",
"description": "%configuration.markdown.experimental.updateLinksOnFileMove.fileGlobs%",
"scope": "resource",
"tags": [
"experimental"
]
}
}
},
Expand Down
5 changes: 5 additions & 0 deletions extensions/markdown-language-features/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,10 @@
"configuration.markdown.experimental.validate.fileLinks.enabled.description": "Validate links to other files in Markdown files, e.g. `[link](/path/to/file.md)`. This checks that the target files exists. Requires enabling `#markdown.experimental.validate.enabled#`.",
"configuration.markdown.experimental.validate.fileLinks.markdownFragmentLinks.description": "Validate the fragment part of links to headers in other files in Markdown files, e.g. `[link](/path/to/file.md#header)`. Inherits the setting value from `#markdown.experimental.validate.fragmentLinks.enabled#` by default.",
"configuration.markdown.experimental.validate.ignoreLinks.description": "Configure links that should not be validated. For example `/about` would not validate the link `[about](/about)`, while the glob `/assets/**/*.svg` would let you skip validation for any link to `.svg` files under the `assets` directory.",
"configuration.markdown.experimental.updateLinksOnFileMove.enabled": "Try to update links in Markdown files when a file is renamed/moved in the workspace. Use `#markdown.experimental.updateLinksOnFileMove.externalFileGlobs#` to configure which files trigger link updates.",
"configuration.markdown.experimental.updateLinksOnFileMove.enabled.prompt": "Prompt on each file move.",
"configuration.markdown.experimental.updateLinksOnFileMove.enabled.always": "Always update links automatically.",
"configuration.markdown.experimental.updateLinksOnFileMove.enabled.never": "Never try to update link and don't prompt.",
"configuration.markdown.experimental.updateLinksOnFileMove.fileGlobs": "A glob that specifies which files besides markdown should trigger a link update.",
"workspaceTrust": "Required for loading styles configured in the workspace."
}
2 changes: 1 addition & 1 deletion extensions/markdown-language-features/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"vscode-languageserver": "^8.0.2",
"vscode-languageserver-textdocument": "^1.0.5",
"vscode-languageserver-types": "^3.17.1",
"vscode-markdown-languageservice": "^0.0.0-alpha.13",
"vscode-markdown-languageservice": "^0.0.0-alpha.14",
"vscode-uri": "^3.0.3"
},
"devDependencies": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('

//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const getEditForFileRenames = new RequestType<Array<{ oldUri: string; newUri: string }>, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames');

export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion
9 changes: 9 additions & 0 deletions extensions/markdown-language-features/server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,15 @@ export async function startServer(connection: Connection) {
return undefined;
}));

connection.onRequest(protocol.getEditForFileRenames, (async (params, token: CancellationToken) => {
try {
return await provider!.getRenameFilesInWorkspaceEdit(params.map(x => ({ oldUri: URI.parse(x.oldUri), newUri: URI.parse(x.newUri) })), token);
} catch (e) {
console.error(e.stack);
}
return undefined;
}));

documents.listen(connection);
notebooks.listen(connection);
connection.listen();
Expand Down
8 changes: 4 additions & 4 deletions extensions/markdown-language-features/server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ vscode-languageserver@^8.0.2:
dependencies:
vscode-languageserver-protocol "3.17.2"

vscode-markdown-languageservice@^0.0.0-alpha.13:
version "0.0.0-alpha.13"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.13.tgz#28cd8dd8eca451aaa3db1c92ec97ace53623dd5d"
integrity sha512-jgRVBQmdO0aC5Svap1RcAd3x2XOSNWla01GF/rzaVx9M5pEcel4SPz+2H9PYXul6jRKe1oKJF9OOciaiE7pSXQ==
vscode-markdown-languageservice@^0.0.0-alpha.14:
version "0.0.0-alpha.14"
resolved "https://registry.yarnpkg.com/vscode-markdown-languageservice/-/vscode-markdown-languageservice-0.0.0-alpha.14.tgz#befe2fd1571213db0abbd9c93a4b9adf22f68d5c"
integrity sha512-6rxEZKnYTJfZBOIWfPeUm5cjss7hgnJ7lQ8ZA4b918SjcOlDT0NOCQZ/88vMuxWdKKQCywcD9YoXNMRYsT+N5w==
dependencies:
picomatch "^2.3.1"
vscode-languageserver-textdocument "^1.0.5"
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 @@ -11,6 +11,7 @@ import { registerPasteSupport } from './languageFeatures/copyPaste';
import { registerDiagnosticSupport } from './languageFeatures/diagnostics';
import { registerDropIntoEditorSupport } from './languageFeatures/dropIntoEditor';
import { registerFindFileReferenceSupport } from './languageFeatures/fileReferences';
import { registerUpdatePathsOnRename } from './languageFeatures/updatePathsOnRename';
import { ILogger } from './logging';
import { MarkdownItEngine, MdParsingProvider } from './markdownEngine';
import { MarkdownContributionProvider } from './markdownExtensions';
Expand Down Expand Up @@ -62,6 +63,7 @@ function registerMarkdownLanguageFeatures(
registerDropIntoEditorSupport(selector),
registerFindFileReferenceSupport(commandManager, client),
registerPasteSupport(selector),
registerUpdatePathsOnRename(client),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import type * as lsp from 'vscode-languageserver-types';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commandManager';
import { getReferencesToFileInWorkspace } from '../protocol';
Expand Down Expand Up @@ -35,7 +36,7 @@ export class FindFileReferencesCommand implements Command {
title: localize('progress.title', "Finding file references")
}, async (_progress, token) => {
const locations = (await this.client.sendRequest(getReferencesToFileInWorkspace, { uri: resource!.toString() }, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), new vscode.Range(loc.range.start.line, loc.range.start.character, loc.range.end.line, loc.range.end.character));
return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range));
});

const config = vscode.workspace.getConfiguration('references');
Expand All @@ -51,6 +52,10 @@ export class FindFileReferencesCommand implements Command {
}
}

export function convertRange(range: lsp.Range): vscode.Range {
return new vscode.Range(range.start.line, range.start.character, range.end.line, range.end.character);
}

export function registerFindFileReferenceSupport(
commandManager: CommandManager,
client: BaseLanguageClient,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as picomatch from 'picomatch';
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { getEditForFileRenames } from '../protocol';
import { Delayer } from '../util/async';
import { noopToken } from '../util/cancellation';
import { Disposable } from '../util/dispose';
import { looksLikeMarkdownPath } from '../util/file';
import { convertRange } from './fileReferences';

const localize = nls.loadMessageBundle();

const settingNames = Object.freeze({
enabled: 'experimental.updateLinksOnFileMove.enabled',
externalFileGlobs: 'experimental.updateLinksOnFileMove.externalFileGlobs'
});

const enum UpdateLinksOnFileMoveSetting {
Prompt = 'prompt',
Always = 'always',
Never = 'never',
}

interface RenameAction {
readonly oldUri: vscode.Uri;
readonly newUri: vscode.Uri;
}

class UpdateImportsOnFileRenameHandler extends Disposable {

private readonly _delayer = new Delayer(50);
private readonly _pendingRenames = new Set<RenameAction>();

public constructor(
private readonly client: BaseLanguageClient,
) {
super();

this._register(vscode.workspace.onDidRenameFiles(async (e) => {
const [{ newUri, oldUri }] = e.files; // TODO: only handles first file

const config = this.getConfiguration(newUri);

const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
if (setting === UpdateLinksOnFileMoveSetting.Never) {
return;
}

if (!this.shouldParticipateInLinkUpdate(config, newUri)) {
return;
}

this._pendingRenames.add({ oldUri, newUri });

this._delayer.trigger(() => {
vscode.window.withProgress({
location: vscode.ProgressLocation.Window,
title: localize('renameProgress.title', "Checking for Markdown links to update")
}, () => this.flushRenames());
});
}));
}

private async flushRenames(): Promise<void> {
const renames = Array.from(this._pendingRenames);
this._pendingRenames.clear();

const edit = new vscode.WorkspaceEdit();
const resourcesBeingRenamed: vscode.Uri[] = [];

for (const { oldUri, newUri } of renames) {
if (await this.withEditsForFileRename(edit, oldUri, newUri, noopToken)) {
resourcesBeingRenamed.push(newUri);
}
}

if (edit.size) {
if (await this.confirmActionWithUser(resourcesBeingRenamed)) {
await vscode.workspace.applyEdit(edit);
}
}
}

private async confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}

const config = this.getConfiguration(newResources[0]);
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
switch (setting) {
case UpdateLinksOnFileMoveSetting.Prompt:
return this.promptUser(newResources);
case UpdateLinksOnFileMoveSetting.Always:
return true;
case UpdateLinksOnFileMoveSetting.Never:
default:
return false;
}
}

private getConfiguration(resource: vscode.Uri) {
return vscode.workspace.getConfiguration('markdown', resource);
}

private shouldParticipateInLinkUpdate(config: vscode.WorkspaceConfiguration, newUri: vscode.Uri) {
if (looksLikeMarkdownPath(newUri)) {
return true;
}

const externalGlob = config.get<string>(settingNames.externalFileGlobs);
return !!externalGlob && picomatch.isMatch(newUri.fsPath, externalGlob);
}

private async promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}

const enum Choice {
None = 0,
Accept = 1,
Reject = 2,
Always = 3,
Never = 4,
}

interface Item extends vscode.MessageItem {
readonly choice: Choice;
}

const response = await vscode.window.showInformationMessage<Item>(
newResources.length === 1
? localize('prompt', "Update Markdown links for '{0}'?", path.basename(newResources[0].fsPath))
: this.getConfirmMessage(localize('promptMoreThanOne', "Update Markdown link for the following {0} files?", newResources.length), newResources), {
modal: true,
}, {
title: localize('reject.title', "No"),
choice: Choice.Reject,
isCloseAffordance: true,
}, {
title: localize('accept.title', "Yes"),
choice: Choice.Accept,
}, {
title: localize('always.title', "Always automatically update Markdown Links"),
choice: Choice.Always,
}, {
title: localize('never.title', "Never automatically update Markdown Links"),
choice: Choice.Never,
});

if (!response) {
return false;
}

switch (response.choice) {
case Choice.Accept: {
return true;
}
case Choice.Reject: {
return false;
}
case Choice.Always: {
const config = this.getConfiguration(newResources[0]);
config.update(
settingNames.enabled,
UpdateLinksOnFileMoveSetting.Always,
vscode.ConfigurationTarget.Global);
return true;
}
case Choice.Never: {
const config = this.getConfiguration(newResources[0]);
config.update(
settingNames.enabled,
UpdateLinksOnFileMoveSetting.Never,
vscode.ConfigurationTarget.Global);
return false;
}
}

return false;
}

private async withEditsForFileRename(
workspaceEdit: vscode.WorkspaceEdit,
oldUri: vscode.Uri,
newUri: vscode.Uri,
token: vscode.CancellationToken,
): Promise<boolean> {
const edit = await this.client.sendRequest(getEditForFileRenames, [{ oldUri: oldUri.toString(), newUri: newUri.toString() }], token);
if (!edit.changes) {
return false;
}

for (const [path, edits] of Object.entries(edit.changes)) {
const uri = vscode.Uri.parse(path);
for (const edit of edits) {
workspaceEdit.replace(uri, convertRange(edit.range), edit.newText);
}
}

return true;
}

private getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
const MAX_CONFIRM_FILES = 10;

const paths = [start];
paths.push('');
paths.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => path.basename(r.fsPath)));

if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
paths.push(localize('moreFile', "...1 additional file not shown"));
} else {
paths.push(localize('moreFiles', "...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
}
}

paths.push('');
return paths.join('\n');
}
}

export function registerUpdatePathsOnRename(client: BaseLanguageClient) {
return new UpdateImportsOnFileRenameHandler(client);
}
1 change: 1 addition & 0 deletions extensions/markdown-language-features/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const findMarkdownFilesInWorkspace = new RequestType<{}, string[], any>('

//#region To server
export const getReferencesToFileInWorkspace = new RequestType<{ uri: string }, lsp.Location[], any>('markdown/getReferencesToFileInWorkspace');
export const getEditForFileRenames = new RequestType<Array<{ oldUri: string; newUri: string }>, lsp.WorkspaceEdit, any>('markdown/getEditForFileRenames');

export const fs_watcher_onChange = new RequestType<{ id: number; uri: string; kind: 'create' | 'change' | 'delete' }, void, any>('markdown/fs/watcher/onChange');
//#endregion