Experiment with switching markdown extension to use native privates

Let's try this out with one extension to start
This commit is contained in:
Matt Bierner
2026-03-10 23:13:16 -07:00
parent 6597286e32
commit 7df46143a1
43 changed files with 845 additions and 690 deletions

View File

@@ -36,14 +36,18 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
...Object.values(rootMediaMimesTypes).map(type => `${type}/*`),
];
private readonly _yieldTo = [
readonly #yieldTo = [
vscode.DocumentDropOrPasteEditKind.Text,
vscode.DocumentDropOrPasteEditKind.Empty.append('markdown', 'link', 'image', 'attachment'), // Prefer notebook attachments
];
readonly #parser: IMdParser;
constructor(
private readonly _parser: IMdParser,
) { }
parser: IMdParser,
) {
this.#parser = parser;
}
public async provideDocumentDropEdits(
document: vscode.TextDocument,
@@ -51,8 +55,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentDropEdit | undefined> {
const edit = await this._createEdit(document, [new vscode.Range(position, position)], dataTransfer, {
insert: this._getEnabled(document, 'editor.drop.enabled'),
const edit = await this.#createEdit(document, [new vscode.Range(position, position)], dataTransfer, {
insert: this.#getEnabled(document, 'editor.drop.enabled'),
copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.drop.copyIntoWorkspace', CopyFilesSettings.MediaFiles)
}, undefined, token);
@@ -64,7 +68,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
dropEdit.title = edit.label;
dropEdit.kind = edit.kind;
dropEdit.additionalEdit = edit.additionalEdits;
dropEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
dropEdit.yieldTo = [...this.#yieldTo, ...edit.yieldTo];
return dropEdit;
}
@@ -75,8 +79,8 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit[] | undefined> {
const edit = await this._createEdit(document, ranges, dataTransfer, {
insert: this._getEnabled(document, 'editor.paste.enabled'),
const edit = await this.#createEdit(document, ranges, dataTransfer, {
insert: this.#getEnabled(document, 'editor.paste.enabled'),
copyIntoWorkspace: vscode.workspace.getConfiguration('markdown', document).get<CopyFilesSettings>('editor.paste.copyIntoWorkspace', CopyFilesSettings.MediaFiles)
}, context, token);
@@ -86,11 +90,11 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
const pasteEdit = new vscode.DocumentPasteEdit(edit.snippet, edit.label, edit.kind);
pasteEdit.additionalEdit = edit.additionalEdits;
pasteEdit.yieldTo = [...this._yieldTo, ...edit.yieldTo];
pasteEdit.yieldTo = [...this.#yieldTo, ...edit.yieldTo];
return [pasteEdit];
}
private _getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink {
#getEnabled(document: vscode.TextDocument, settingName: string): InsertMarkdownLink {
const setting = vscode.workspace.getConfiguration('markdown', document).get<boolean | InsertMarkdownLink>(settingName, true);
// Convert old boolean values to new enum setting
if (setting === false) {
@@ -102,7 +106,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
}
}
private async _createEdit(
async #createEdit(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
@@ -117,27 +121,27 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
return;
}
let edit = await this._createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token);
let edit = await this.#createEditForMediaFiles(document, dataTransfer, settings.copyIntoWorkspace, token);
if (token.isCancellationRequested) {
return;
}
if (!edit) {
edit = await this._createEditFromUriListData(document, ranges, dataTransfer, context, token);
edit = await this.#createEditFromUriListData(document, ranges, dataTransfer, context, token);
}
if (!edit || token.isCancellationRequested) {
return;
}
if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, settings.insert, ranges, token))) {
if (!(await shouldInsertMarkdownLinkByDefault(this.#parser, document, settings.insert, ranges, token))) {
edit.yieldTo.push(vscode.DocumentDropOrPasteEditKind.Empty.append('uri'));
}
return edit;
}
private async _createEditFromUriListData(
async #createEditFromUriListData(
document: vscode.TextDocument,
ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
@@ -194,7 +198,7 @@ class ResourcePasteOrDropProvider implements vscode.DocumentPasteEditProvider, v
*
* This tries copying files outside of the workspace into the workspace.
*/
private async _createEditForMediaFiles(
async #createEditForMediaFiles(
document: vscode.TextDocument,
dataTransfer: vscode.DataTransfer,
copyIntoWorkspace: CopyFilesSettings,

View File

@@ -12,7 +12,7 @@ import { CopyFileConfiguration, getCopyFileConfiguration, parseGlob, resolveCopy
export class NewFilePathGenerator {
private readonly _usedPaths = new Set<string>();
readonly #usedPaths = new Set<string>();
async getNewFilePath(
document: vscode.TextDocument,
@@ -33,13 +33,13 @@ export class NewFilePathGenerator {
const name = i === 0 ? baseName : `${baseName}-${i}`;
const uri = vscode.Uri.joinPath(root, name + ext);
if (this._wasPathAlreadyUsed(uri)) {
if (this.#wasPathAlreadyUsed(uri)) {
continue;
}
// Try overwriting if it already exists
if (config.overwriteBehavior === 'overwrite') {
this._usedPaths.add(uri.toString());
this.#usedPaths.add(uri.toString());
return { uri, overwrite: true };
}
@@ -47,17 +47,17 @@ export class NewFilePathGenerator {
try {
await vscode.workspace.fs.stat(uri);
} catch {
if (!this._wasPathAlreadyUsed(uri)) {
if (!this.#wasPathAlreadyUsed(uri)) {
// Does not exist
this._usedPaths.add(uri.toString());
this.#usedPaths.add(uri.toString());
return { uri, overwrite: false };
}
}
}
}
private _wasPathAlreadyUsed(uri: vscode.Uri) {
return this._usedPaths.has(uri.toString());
#wasPathAlreadyUsed(uri: vscode.Uri) {
return this.#usedPaths.has(uri.toString());
}
}

View File

@@ -21,9 +21,13 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
public static readonly pasteMimeTypes = [Mime.textPlain];
readonly #parser: IMdParser;
constructor(
private readonly _parser: IMdParser,
) { }
parser: IMdParser,
) {
this.#parser = parser;
}
async provideDocumentPasteEdits(
document: vscode.TextDocument,
@@ -64,7 +68,7 @@ class PasteUrlEditProvider implements vscode.DocumentPasteEditProvider {
workspaceEdit.set(document.uri, edit.edits);
pasteEdit.additionalEdit = workspaceEdit;
if (!(await shouldInsertMarkdownLinkByDefault(this._parser, document, pasteUrlSetting, ranges, token))) {
if (!(await shouldInsertMarkdownLinkByDefault(this.#parser, document, pasteUrlSetting, ranges, token))) {
pasteEdit.yieldTo = [
vscode.DocumentDropOrPasteEditKind.Text,
vscode.DocumentDropOrPasteEditKind.Empty.append('uri')

View File

@@ -19,18 +19,18 @@ export enum DiagnosticCode {
class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks';
static readonly #addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks';
private static readonly _metadata: vscode.CodeActionProviderMetadata = {
static readonly #metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix
],
};
public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable {
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider._metadata);
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.#metadata);
const commandReg = commandManager.register({
id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
id: AddToIgnoreLinksQuickFixProvider.#addToIgnoreLinksCommandId,
execute(resource: vscode.Uri, path: string) {
const settingId = 'validate.ignoredLinks';
const config = vscode.workspace.getConfiguration('markdown', resource);
@@ -58,7 +58,7 @@ class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
vscode.CodeActionKind.QuickFix);
fix.command = {
command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
command: AddToIgnoreLinksQuickFixProvider.#addToIgnoreLinksCommandId,
title: '',
arguments: [document.uri, hrefText],
};

View File

@@ -13,9 +13,13 @@ export class FindFileReferencesCommand implements Command {
public readonly id = 'markdown.findAllFileReferences';
readonly #client: MdLanguageClient;
constructor(
private readonly _client: MdLanguageClient,
) { }
client: MdLanguageClient,
) {
this.#client = client;
}
public async execute(resource?: vscode.Uri) {
resource ??= vscode.window.activeTextEditor?.document.uri;
@@ -28,7 +32,7 @@ export class FindFileReferencesCommand implements Command {
location: vscode.ProgressLocation.Window,
title: vscode.l10n.t("Finding file references")
}, async (_progress, token) => {
const locations = (await this._client.getReferencesToFileInWorkspace(resource, token)).map(loc => {
const locations = (await this.#client.getReferencesToFileInWorkspace(resource, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), convertRange(loc.range));
});

View File

@@ -33,46 +33,48 @@ interface RenameAction {
class UpdateLinksOnFileRenameHandler extends Disposable {
private readonly _delayer = new Delayer(50);
private readonly _pendingRenames = new Set<RenameAction>();
readonly #delayer = new Delayer(50);
readonly #pendingRenames = new Set<RenameAction>();
readonly #client: MdLanguageClient;
public constructor(
private readonly _client: MdLanguageClient,
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 (await this.#shouldParticipateInLinkUpdate(rename.newUri)) {
this.#pendingRenames.add(rename);
}
}));
if (this._pendingRenames.size) {
this._delayer.trigger(() => {
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());
}, () => this.#flushRenames());
});
}
}));
}
private async _flushRenames(): Promise<void> {
const renames = Array.from(this._pendingRenames);
this._pendingRenames.clear();
async #flushRenames(): Promise<void> {
const renames = Array.from(this.#pendingRenames);
this.#pendingRenames.clear();
const result = await this._getEditsForFileRename(renames, noopToken);
const result = await this.#getEditsForFileRename(renames, noopToken);
if (result?.edit.size) {
if (await this._confirmActionWithUser(result.resourcesBeingRenamed)) {
if (await this.#confirmActionWithUser(result.resourcesBeingRenamed)) {
await vscode.workspace.applyEdit(result.edit);
}
}
}
private async _confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
async #confirmActionWithUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
@@ -81,7 +83,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
const setting = config.get<UpdateLinksOnFileMoveSetting>(settingNames.enabled);
switch (setting) {
case UpdateLinksOnFileMoveSetting.Prompt:
return this._promptUser(newResources);
return this.#promptUser(newResources);
case UpdateLinksOnFileMoveSetting.Always:
return true;
case UpdateLinksOnFileMoveSetting.Never:
@@ -89,7 +91,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
return false;
}
}
private async _shouldParticipateInLinkUpdate(newUri: vscode.Uri): Promise<boolean> {
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) {
@@ -113,7 +115,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
return false;
}
private async _promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
async #promptUser(newResources: readonly vscode.Uri[]): Promise<boolean> {
if (!newResources.length) {
return false;
}
@@ -138,7 +140,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
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), {
: this.#getConfirmMessage(vscode.l10n.t("Update Markdown links for the following {0} files?", newResources.length), newResources), {
modal: true,
}, rejectItem, acceptItem, alwaysItem, neverItem);
@@ -154,7 +156,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
config.update(
settingNames.enabled,
UpdateLinksOnFileMoveSetting.Always,
this._getConfigTargetScope(config, settingNames.enabled));
this.#getConfigTargetScope(config, settingNames.enabled));
return true;
}
case neverItem: {
@@ -162,7 +164,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
config.update(
settingNames.enabled,
UpdateLinksOnFileMoveSetting.Never,
this._getConfigTargetScope(config, settingNames.enabled));
this.#getConfigTargetScope(config, settingNames.enabled));
return false;
}
default: {
@@ -171,8 +173,8 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
}
}
private 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);
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;
}
@@ -192,7 +194,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
};
}
private _getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
#getConfirmMessage(start: string, resourcesToConfirm: readonly vscode.Uri[]): string {
const MAX_CONFIRM_FILES = 10;
const paths = [start];
@@ -211,7 +213,7 @@ class UpdateLinksOnFileRenameHandler extends Disposable {
return paths.join('\n');
}
private _getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget {
#getConfigTargetScope(config: vscode.WorkspaceConfiguration, settingsName: string): vscode.ConfigurationTarget {
const inspected = config.inspect(settingsName);
if (inspected?.workspaceFolderValue) {
return vscode.ConfigurationTarget.WorkspaceFolder;

View File

@@ -13,16 +13,20 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider
public static readonly metadataMime = 'application/vnd.vscode.markdown.updatelinks.metadata';
readonly #client: MdLanguageClient;
constructor(
private readonly _client: MdLanguageClient,
) { }
client: MdLanguageClient,
) {
this.#client = client;
}
async prepareDocumentPaste(document: vscode.TextDocument, ranges: readonly vscode.Range[], dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<void> {
if (!this._isEnabled(document)) {
if (!this.#isEnabled(document)) {
return;
}
const metadata = await this._client.prepareUpdatePastedLinks(document.uri, ranges, token);
const metadata = await this.#client.prepareUpdatePastedLinks(document.uri, ranges, token);
if (token.isCancellationRequested) {
return;
}
@@ -37,7 +41,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider
context: vscode.DocumentPasteEditContext,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit[] | undefined> {
if (!this._isEnabled(document)) {
if (!this.#isEnabled(document)) {
return;
}
@@ -56,7 +60,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider
// - copy empty line
// - Copy with multiple cursors and paste into multiple locations
// - ...
const edits = await this._client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token);
const edits = await this.#client.getUpdatePastedLinksEdit(document.uri, ranges.map(x => new vscode.TextEdit(x, text)), metadata, token);
if (!edits?.length || token.isCancellationRequested) {
return;
}
@@ -73,7 +77,7 @@ class UpdatePastedLinksEditProvider implements vscode.DocumentPasteEditProvider
return [pasteEdit];
}
private _isEnabled(document: vscode.TextDocument): boolean {
#isEnabled(document: vscode.TextDocument): boolean {
return vscode.workspace.getConfiguration('markdown', document.uri).get<boolean>('editor.updateLinksOnPaste.enabled', true);
}
}