Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/markdown-language-features/src/languageFeatures/linkUpdater.ts
3292 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as path from 'path';
7
import * as picomatch from 'picomatch';
8
import * as vscode from 'vscode';
9
import { TextDocumentEdit } from 'vscode-languageclient';
10
import { MdLanguageClient } from '../client/client';
11
import { Delayer } from '../util/async';
12
import { noopToken } from '../util/cancellation';
13
import { Disposable } from '../util/dispose';
14
import { convertRange } from './fileReferences';
15
16
17
const settingNames = Object.freeze({
18
enabled: 'updateLinksOnFileMove.enabled',
19
include: 'updateLinksOnFileMove.include',
20
enableForDirectories: 'updateLinksOnFileMove.enableForDirectories',
21
});
22
23
const enum UpdateLinksOnFileMoveSetting {
24
Prompt = 'prompt',
25
Always = 'always',
26
Never = 'never',
27
}
28
29
interface RenameAction {
30
readonly oldUri: vscode.Uri;
31
readonly newUri: vscode.Uri;
32
}
33
34
class UpdateLinksOnFileRenameHandler extends Disposable {
35
36
private readonly _delayer = new Delayer(50);
37
private readonly _pendingRenames = new Set<RenameAction>();
38
39
public constructor(
40
private readonly _client: MdLanguageClient,
41
) {
42
super();
43
44
this._register(vscode.workspace.onDidRenameFiles(async (e) => {
45
await Promise.all(e.files.map(async (rename) => {
46
if (await this._shouldParticipateInLinkUpdate(rename.newUri)) {
47
this._pendingRenames.add(rename);
48
}
49
}));
50
51
if (this._pendingRenames.size) {
52
this._delayer.trigger(() => {
53
vscode.window.withProgress({
54
location: vscode.ProgressLocation.Window,
55
title: vscode.l10n.t("Checking for Markdown links to update")
56
}, () => this._flushRenames());
57
});
58
}
59
}));
60
}
61
62
private async _flushRenames(): Promise<void> {
63
const renames = Array.from(this._pendingRenames);
64
this._pendingRenames.clear();
65
66
const result = await this._getEditsForFileRename(renames, noopToken);
67
68
if (result?.edit.size) {
69
if (await this._confirmActionWithUser(result.resourcesBeingRenamed)) {
70
await vscode.workspace.applyEdit(result.edit);
71
}
72
}
73
}
74
75
private async _confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
76
if (!newResources.length) {
77
return false;
78
}
79
80
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
81
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
82
switch (setting) {
83
case UpdateLinksOnFileMoveSetting.Prompt:
84
return this._promptUser(newResources);
85
case UpdateLinksOnFileMoveSetting.Always:
86
return true;
87
case UpdateLinksOnFileMoveSetting.Never:
88
default:
89
return false;
90
}
91
}
92
private async _shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise<boolean> {
93
const config = vscode.workspace.getConfiguration('markdown', newUri);
94
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
95
if (setting === UpdateLinksOnFileMoveSetting.Never) {
96
return false;
97
}
98
99
const externalGlob = config.get<string[]>(settingNames.include);
100
if (externalGlob) {
101
for (const glob of externalGlob) {
102
if (picomatch.isMatch(newUri.fsPath, glob)) {
103
return true;
104
}
105
}
106
}
107
108
const stat = await vscode.workspace.fs.stat(newUri);
109
if (stat.type === vscode.FileType.Directory) {
110
return config.get<boolean>(settingNames.enableForDirectories, true);
111
}
112
113
return false;
114
}
115
116
private async _promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
117
if (!newResources.length) {
118
return false;
119
}
120
121
const rejectItem: vscode.MessageItem = {
122
title: vscode.l10n.t("No"),
123
isCloseAffordance: true,
124
};
125
126
const acceptItem: vscode.MessageItem = {
127
title: vscode.l10n.t("Yes"),
128
};
129
130
const alwaysItem: vscode.MessageItem = {
131
title: vscode.l10n.t("Always"),
132
};
133
134
const neverItem: vscode.MessageItem = {
135
title: vscode.l10n.t("Never"),
136
};
137
138
const choice = await vscode.window.showInformationMessage(
139
newResources.length === 1
140
? vscode.l10n.t("Update Markdown links for '{0}'?", path.basename(newResources[0].fsPath))
141
: this._getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), {
142
modal: true,
143
}, rejectItem, acceptItem, alwaysItem, neverItem);
144
145
switch (choice) {
146
case acceptItem: {
147
return true;
148
}
149
case rejectItem: {
150
return false;
151
}
152
case alwaysItem: {
153
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
154
config.update(
155
settingNames.enabled,
156
UpdateLinksOnFileMoveSetting.Always,
157
this._getConfigTargetScope(config, settingNames.enabled));
158
return true;
159
}
160
case neverItem: {
161
const config = vscode.workspace.getConfiguration('markdown', newResources[0]);
162
config.update(
163
settingNames.enabled,
164
UpdateLinksOnFileMoveSetting.Never,
165
this._getConfigTargetScope(config, settingNames.enabled));
166
return false;
167
}
168
default: {
169
return false;
170
}
171
}
172
}
173
174
private async _getEditsForFileRename(renames: readonly RenameAction[], token: vscode.CancellationToken): Promise<{ edit: vscode.WorkspaceEdit; resourcesBeingRenamed: vscode.Uri[] } | undefined> {
175
const result = await this._client.getEditForFileRenames(renames.map(rename => ({ oldUri: rename.oldUri.toString(), newUri: rename.newUri.toString() })), token);
176
if (!result?.edit.documentChanges?.length) {
177
return undefined;
178
}
179
180
const workspaceEdit = new vscode.WorkspaceEdit();
181
182
for (const change of result.edit.documentChanges as TextDocumentEdit[]) {
183
const uri = vscode.Uri.parse(change.textDocument.uri);
184
for (const edit of change.edits) {
185
workspaceEdit.replace(uri, convertRange(edit.range), edit.newText);
186
}
187
}
188
189
return {
190
edit: workspaceEdit,
191
resourcesBeingRenamed: result.participatingRenames.map(x => vscode.Uri.parse(x.newUri)),
192
};
193
}
194
195
private _getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
196
const MAX_CONFIRM_FILES = 10;
197
198
const paths = [start];
199
paths.push('');
200
paths.push(...resourcesToConfirm.slice(0, MAX_CONFIRM_FILES).map(r => path.basename(r.fsPath)));
201
202
if (resourcesToConfirm.length > MAX_CONFIRM_FILES) {
203
if (resourcesToConfirm.length - MAX_CONFIRM_FILES === 1) {
204
paths.push(vscode.l10n.t("...1 additional file not shown"));
205
} else {
206
paths.push(vscode.l10n.t("...{0} additional files not shown", resourcesToConfirm.length - MAX_CONFIRM_FILES));
207
}
208
}
209
210
paths.push('');
211
return paths.join('\n');
212
}
213
214
private _getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget {
215
const inspected = config.inspect(settingsName);
216
if (inspected?.workspaceFolderValue) {
217
return vscode.ConfigurationTarget.WorkspaceFolder;
218
}
219
220
if (inspected?.workspaceValue) {
221
return vscode.ConfigurationTarget.Workspace;
222
}
223
224
return vscode.ConfigurationTarget.Global;
225
}
226
}
227
228
export function registerUpdateLinksOnRename(client: MdLanguageClient): vscode.Disposable {
229
return new UpdateLinksOnFileRenameHandler(client);
230
}
231
232