mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 05:19:39 +00:00
Add experimental support for update markdown links on file moves/renames (#157209)
* Add experimental support for update markdown links on file moves/renames Fixes #148146 This adds a new experimental setting that automatically updates markdown Note that this needs a new version of the vscode-markdown-languageservice so the build is expected to break for now * Pick up new LS version
This commit is contained in:
parent
81e6a02c18
commit
8bf82819fc
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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';
|
||||
|
@ -62,6 +63,7 @@ function registerMarkdownLanguageFeatures(
|
|||
registerDropIntoEditorSupport(selector),
|
||||
registerFindFileReferenceSupport(commandManager, client),
|
||||
registerPasteSupport(selector),
|
||||
registerUpdatePathsOnRename(client),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue