mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-20 08:38:56 +01:00
233 lines
7.0 KiB
TypeScript
233 lines
7.0 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as picomatch from 'picomatch';
|
|
import * as vscode from 'vscode';
|
|
import { TextDocumentEdit } from 'vscode-languageclient';
|
|
import { Utils } from 'vscode-uri';
|
|
import { MdLanguageClient } from '../client/client';
|
|
import { Delayer } from '../util/async';
|
|
import { noopToken } from '../util/cancellation';
|
|
import { Disposable } from '../util/dispose';
|
|
import { convertRange } from './fileReferences';
|
|
|
|
|
|
const settingNames = Object.freeze({
|
|
enabled: 'updateLinksOnFileMove.enabled',
|
|
include: 'updateLinksOnFileMove.include',
|
|
enableForDirectories: 'updateLinksOnFileMove.enableForDirectories',
|
|
});
|
|
|
|
const enum UpdateLinksOnFileMoveSetting {
|
|
Prompt = 'prompt',
|
|
Always = 'always',
|
|
Never = 'never',
|
|
}
|
|
|
|
interface RenameAction {
|
|
readonly oldUri: vscode.Uri;
|
|
readonly newUri: vscode.Uri;
|
|
}
|
|
|
|
class UpdateLinksOnFileRenameHandler extends Disposable {
|
|
|
|
readonly #delayer = new Delayer(50);
|
|
readonly #pendingRenames = new Set<RenameAction>();
|
|
readonly #client: MdLanguageClient;
|
|
|
|
public constructor(
|
|
client: MdLanguageClient,
|
|
) {
|
|
super();
|
|
this.#client = client;
|
|
|
|
this._register(vscode.workspace.onDidRenameFiles(async (e) => {
|
|
await Promise.all(e.files.map(async (rename) => {
|
|
if (await this.#shouldParticipateInLinkUpdate(rename.newUri)) {
|
|
this.#pendingRenames.add(rename);
|
|
}
|
|
}));
|
|
|
|
if (this.#pendingRenames.size) {
|
|
this.#delayer.trigger(() => {
|
|
vscode.window.withProgress({
|
|
location: vscode.ProgressLocation.Window,
|
|
title: vscode.l10n.t("Checking for Markdown links to update")
|
|
}, () => this.#flushRenames());
|
|
});
|
|
}
|
|
}));
|
|
}
|
|
|
|
async #flushRenames(): Promise<void> {
|
|
const renames = Array.from(this.#pendingRenames);
|
|
this.#pendingRenames.clear();
|
|
|
|
const result = await this.#getEditsForFileRename(renames, noopToken);
|
|
|
|
if (result?.edit.size) {
|
|
if (await this.#confirmActionWithUser(result.resourcesBeingRenamed)) {
|
|
await vscode.workspace.applyEdit(result.edit);
|
|
}
|
|
}
|
|
}
|
|
|
|
async #confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
|
|
if (!newResources.length) {
|
|
return false;
|
|
}
|
|
|
|
const config = vscode.workspace.getConfiguration('markdown', 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;
|
|
}
|
|
}
|
|
async #shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise<boolean> {
|
|
const config = vscode.workspace.getConfiguration('markdown', newUri);
|
|
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
|
|
if (setting === UpdateLinksOnFileMoveSetting.Never) {
|
|
return false;
|
|
}
|
|
|
|
const externalGlob = config.get<string[]>(settingNames.include);
|
|
if (externalGlob) {
|
|
for (const glob of externalGlob) {
|
|
if (picomatch.isMatch(newUri.fsPath, glob)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
const stat = await vscode.workspace.fs.stat(newUri);
|
|
if (stat.type === vscode.FileType.Directory) {
|
|
return config.get<boolean>(settingNames.enableForDirectories, true);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async #promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
|
|
if (!newResources.length) {
|
|
return false;
|
|
}
|
|
|
|
const rejectItem: vscode.MessageItem = {
|
|
title: vscode.l10n.t("No"),
|
|
isCloseAffordance: true,
|
|
};
|
|
|
|
const acceptItem: vscode.MessageItem = {
|
|
title: vscode.l10n.t("Yes"),
|
|
};
|
|
|
|
const alwaysItem: vscode.MessageItem = {
|
|
title: vscode.l10n.t("Always"),
|
|
};
|
|
|
|
const neverItem: vscode.MessageItem = {
|
|
title: vscode.l10n.t("Never"),
|
|
};
|
|
|
|
const choice = await vscode.window.showInformationMessage(
|
|
newResources.length === 1
|
|
? vscode.l10n.t("Update Markdown links for '{0}'?", Utils.basename(newResources[0]))
|
|
: this.#getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), {
|
|
modal: true,
|
|
}, rejectItem, acceptItem, alwaysItem, neverItem);
|
|
|
|
switch (choice) {
|
|
case acceptItem: {
|
|
return true;
|
|
}
|
|
case rejectItem: {
|
|
return false;
|
|
}
|
|
case alwaysItem: {
|
|
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
|
|
config.update(
|
|
settingNames.enabled,
|
|
UpdateLinksOnFileMoveSetting.Always,
|
|
this.#getConfigTargetScope(config, settingNames.enabled));
|
|
return true;
|
|
}
|
|
case neverItem: {
|
|
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
|
|
config.update(
|
|
settingNames.enabled,
|
|
UpdateLinksOnFileMoveSetting.Never,
|
|
this.#getConfigTargetScope(config, settingNames.enabled));
|
|
return false;
|
|
}
|
|
default: {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
async #getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> {
|
|
const result = await this.#client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token);
|
|
if (!result?.edit.documentChanges?.length) {
|
|
return undefined;
|
|
}
|
|
|
|
const workspaceEdit = new vscode.WorkspaceEdit();
|
|
|
|
for (const change of result.edit.documentChanges as TextDocumentEdit[]) {
|
|
const uri = vscode.Uri.parse(change.textDocument.uri);
|
|
for (const edit of change.edits) {
|
|
workspaceEdit.replace(uri, convertRange(edit.range), edit.newText);
|
|
}
|
|
}
|
|
|
|
return {
|
|
edit: workspaceEdit,
|
|
resourcesBeingRenamed: result.participatingRenames.map(x => vscode.Uri.parse(x.newUri)),
|
|
};
|
|
}
|
|
|
|
#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 => Utils.basename(r)));
|
|
|
|
if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
|
|
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
|
|
paths.push(vscode.l10n.t("...1 additional file not shown"));
|
|
} else {
|
|
paths.push(vscode.l10n.t("...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
|
|
}
|
|
}
|
|
|
|
paths.push('');
|
|
return paths.join('\n');
|
|
}
|
|
|
|
#getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget {
|
|
const inspected = config.inspect(settingsName);
|
|
if (inspected?.workspaceFolderValue) {
|
|
return vscode.ConfigurationTarget.WorkspaceFolder;
|
|
}
|
|
|
|
if (inspected?.workspaceValue) {
|
|
return vscode.ConfigurationTarget.Workspace;
|
|
}
|
|
|
|
return vscode.ConfigurationTarget.Global;
|
|
}
|
|
}
|
|
|
|
export function registerUpdateLinksOnRename(client: MdLanguageClient): vscode.Disposable {
|
|
return new UpdateLinksOnFileRenameHandler(client);
|
|
}
|