Path: blob/main/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts
3292 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as path from 'path';6import * as picomatch from 'picomatch';7import * as vscode from 'vscode';8import { TextDocumentEdit } from 'vscode-languageclient';9import { MdLanguageClient } from '../client/client';10import { Delayer } from '../util/async';11import { noopToken } from '../util/cancellation';12import { Disposable } from '../util/dispose';13import { convertRange } from './fileReferences';141516const settingNames = Object.freeze({17enabled: 'updateLinksOnFileMove.enabled',18include: 'updateLinksOnFileMove.include',19enableForDirectories: 'updateLinksOnFileMove.enableForDirectories',20});2122const enum UpdateLinksOnFileMoveSetting {23Prompt = 'prompt',24Always = 'always',25Never = 'never',26}2728interface RenameAction {29readonly oldUri: vscode.Uri;30readonly newUri: vscode.Uri;31}3233class UpdateLinksOnFileRenameHandler extends Disposable {3435private readonly _delayer = new Delayer(50);36private readonly _pendingRenames = new Set<RenameAction>();3738public constructor(39private readonly _client: MdLanguageClient,40) {41super();4243this._register(vscode.workspace.onDidRenameFiles(async (e) => {44await Promise.all(e.files.map(async (rename) => {45if (await this._shouldParticipateInLinkUpdate(rename.newUri)) {46this._pendingRenames.add(rename);47}48}));4950if (this._pendingRenames.size) {51this._delayer.trigger(() => {52vscode.window.withProgress({53location: vscode.ProgressLocation.Window,54title: vscode.l10n.t("Checking for Markdown links to update")55}, () => this._flushRenames());56});57}58}));59}6061private async _flushRenames(): Promise<void> {62const renames = Array.from(this._pendingRenames);63this._pendingRenames.clear();6465const result = await this._getEditsForFileRename(renames, noopToken);6667if (result?.edit.size) {68if (await this._confirmActionWithUser(result.resourcesBeingRenamed)) {69await vscode.workspace.applyEdit(result.edit);70}71}72}7374private async _confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {75if (!newResources.length) {76return false;77}7879const config = vscode.workspace.getConfiguration('markdown', newResources[0]);80const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);81switch (setting) {82case UpdateLinksOnFileMoveSetting.Prompt:83return this._promptUser(newResources);84case UpdateLinksOnFileMoveSetting.Always:85return true;86case UpdateLinksOnFileMoveSetting.Never:87default:88return false;89}90}91private async _shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise<boolean> {92const config = vscode.workspace.getConfiguration('markdown', newUri);93const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);94if (setting === UpdateLinksOnFileMoveSetting.Never) {95return false;96}9798const externalGlob = config.get<string[]>(settingNames.include);99if (externalGlob) {100for (const glob of externalGlob) {101if (picomatch.isMatch(newUri.fsPath, glob)) {102return true;103}104}105}106107const stat = await vscode.workspace.fs.stat(newUri);108if (stat.type === vscode.FileType.Directory) {109return config.get<boolean>(settingNames.enableForDirectories, true);110}111112return false;113}114115private async _promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {116if (!newResources.length) {117return false;118}119120const rejectItem: vscode.MessageItem = {121title: vscode.l10n.t("No"),122isCloseAffordance: true,123};124125const acceptItem: vscode.MessageItem = {126title: vscode.l10n.t("Yes"),127};128129const alwaysItem: vscode.MessageItem = {130title: vscode.l10n.t("Always"),131};132133const neverItem: vscode.MessageItem = {134title: vscode.l10n.t("Never"),135};136137const choice = await vscode.window.showInformationMessage(138newResources.length === 1139? vscode.l10n.t("Update Markdown links for '{0}'?", path.basename(newResources[0].fsPath))140: this._getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), {141modal: true,142}, rejectItem, acceptItem, alwaysItem, neverItem);143144switch (choice) {145case acceptItem: {146return true;147}148case rejectItem: {149return false;150}151case alwaysItem: {152const config = vscode.workspace.getConfiguration('markdown', newResources[0]);153config.update(154settingNames.enabled,155UpdateLinksOnFileMoveSetting.Always,156this._getConfigTargetScope(config, settingNames.enabled));157return true;158}159case neverItem: {160const config = vscode.workspace.getConfiguration('markdown', newResources[0]);161config.update(162settingNames.enabled,163UpdateLinksOnFileMoveSetting.Never,164this._getConfigTargetScope(config, settingNames.enabled));165return false;166}167default: {168return false;169}170}171}172173private async _getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> {174const result = await this._client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token);175if (!result?.edit.documentChanges?.length) {176return undefined;177}178179const workspaceEdit = new vscode.WorkspaceEdit();180181for (const change of result.edit.documentChanges as TextDocumentEdit[]) {182const uri = vscode.Uri.parse(change.textDocument.uri);183for (const edit of change.edits) {184workspaceEdit.replace(uri, convertRange(edit.range), edit.newText);185}186}187188return {189edit: workspaceEdit,190resourcesBeingRenamed: result.participatingRenames.map(x => vscode.Uri.parse(x.newUri)),191};192}193194private _getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {195const MAX_CONFIRM_FILES = 10;196197const paths = [start];198paths.push('');199paths.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => path.basename(r.fsPath)));200201if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {202if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {203paths.push(vscode.l10n.t("...1 additional file not shown"));204} else {205paths.push(vscode.l10n.t("...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));206}207}208209paths.push('');210return paths.join('\n');211}212213private _getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget {214const inspected = config.inspect(settingsName);215if (inspected?.workspaceFolderValue) {216return vscode.ConfigurationTarget.WorkspaceFolder;217}218219if (inspected?.workspaceValue) {220return vscode.ConfigurationTarget.Workspace;221}222223return vscode.ConfigurationTarget.Global;224}225}226227export function registerUpdateLinksOnRename(client: MdLanguageClient): vscode.Disposable {228return new UpdateLinksOnFileRenameHandler(client);229}230231232