Rename skipPaths to ignoreLinks (#150064)

This change renames the experimental skipPaths setting to ignoreLinks. This setting applies to all types of links, and can also be used to ignore links to headers
This commit is contained in:
Matt Bierner 2022-05-20 15:54:21 -07:00 committed by GitHub
parent ecf7f9cfd9
commit 96cae45ba9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 71 additions and 35 deletions

View file

@ -453,10 +453,10 @@
"error"
]
},
"markdown.experimental.validate.fileLinks.skipPaths": {
"markdown.experimental.validate.ignoreLinks": {
"type": "array",
"scope": "resource",
"markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.skipPaths.description%",
"markdownDescription": "%configuration.markdown.experimental.validate.ignoreLinks.description%",
"items": {
"type": "string"
}

View file

@ -33,6 +33,6 @@
"configuration.markdown.experimental.validate.referenceLinks.enabled.description": "Validate reference links in Markdown files, e.g. `[link][ref]`. Requires enabling `#markdown.experimental.validate.enabled#`.",
"configuration.markdown.experimental.validate.headerLinks.enabled.description": "Validate links to headers in Markdown files, e.g. `[link](#header)`. Requires enabling `#markdown.experimental.validate.enabled#`.",
"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.skipPaths.description": "Configure glob patterns for links to treat as valid, even if they don't exist in the workspace. For example `/about` would make the link `[about](/about)` valid, while the glob `/assets/**/*.svg` would let you link to any `.svg` asset under the `assets` directory",
"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.",
"workspaceTrust": "Required for loading styles configured in the workspace."
}

View file

@ -39,7 +39,7 @@ export interface DiagnosticOptions {
readonly validateReferences: DiagnosticLevel;
readonly validateOwnHeaders: DiagnosticLevel;
readonly validateFilePaths: DiagnosticLevel;
readonly skipPaths: readonly string[];
readonly ignoreLinks: readonly string[];
}
function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined {
@ -64,7 +64,7 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf
|| e.affectsConfiguration('markdown.experimental.validate.referenceLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.headerLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.skipPaths')
|| e.affectsConfiguration('markdown.experimental.validate.ignoreLinks')
) {
this._onDidChange.fire();
}
@ -78,7 +78,7 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks.enabled', DiagnosticLevel.ignore),
validateOwnHeaders: config.get<DiagnosticLevel>('experimental.validate.headerLinks.enabled', DiagnosticLevel.ignore),
validateFilePaths: config.get<DiagnosticLevel>('experimental.validate.fileLinks.enabled', DiagnosticLevel.ignore),
skipPaths: config.get('experimental.validate.fileLinks.skipPaths', []),
ignoreLinks: config.get('experimental.validate.ignoreLinks', []),
};
}
}
@ -216,13 +216,13 @@ class LinkWatcher extends Disposable {
}
}
class FileDoesNotExistDiagnostic extends vscode.Diagnostic {
class LinkDoesNotExistDiagnostic extends vscode.Diagnostic {
public readonly path: string;
public readonly link: string;
constructor(range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, path: string) {
constructor(range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, link: string) {
super(range, message, severity);
this.path = path;
this.link = link;
}
}
@ -424,10 +424,13 @@ export class DiagnosticComputer {
&& link.href.fragment
&& !toc.lookup(link.href.fragment)
) {
diagnostics.push(new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
severity));
if (!this.isIgnoredLink(options, link.source.text)) {
diagnostics.push(new LinkDoesNotExistDiagnostic(
link.source.hrefRange,
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
severity,
link.source.text));
}
}
}
@ -481,8 +484,8 @@ export class DiagnosticComputer {
if (!hrefDoc && !await this.workspaceContents.pathExists(path)) {
const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.fsPath);
for (const link of links) {
if (!options.skipPaths.some(glob => picomatch.isMatch(link.source.pathText, glob))) {
diagnostics.push(new FileDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.pathText));
if (!this.isIgnoredLink(options, link.source.pathText)) {
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.pathText));
}
}
} else if (hrefDoc) {
@ -491,9 +494,9 @@ export class DiagnosticComputer {
if (fragmentLinks.length) {
const toc = await TableOfContents.create(this.engine, hrefDoc);
for (const link of fragmentLinks) {
if (!toc.lookup(link.fragment)) {
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.text)) {
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
diagnostics.push(new vscode.Diagnostic(link.source.hrefRange, msg, severity));
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.text));
}
}
}
@ -502,11 +505,15 @@ export class DiagnosticComputer {
}));
return diagnostics;
}
private isIgnoredLink(options: DiagnosticOptions, link: string): boolean {
return options.ignoreLinks.some(glob => picomatch.isMatch(link, glob));
}
}
class AddToSkipPathsQuickFixProvider implements vscode.CodeActionProvider {
class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
private static readonly _addToSkipPathsCommandId = '_markdown.addToSkipPaths';
private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks';
private static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [
@ -515,11 +522,11 @@ class AddToSkipPathsQuickFixProvider implements vscode.CodeActionProvider {
};
public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable {
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToSkipPathsQuickFixProvider(), AddToSkipPathsQuickFixProvider.metadata);
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.metadata);
const commandReg = commandManager.register({
id: AddToSkipPathsQuickFixProvider._addToSkipPathsCommandId,
id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
execute(resource: vscode.Uri, path: string) {
const settingId = 'experimental.validate.fileLinks.skipPaths';
const settingId = 'experimental.validate.ignoreLinks';
const config = vscode.workspace.getConfiguration('markdown', resource);
const paths = new Set(config.get<string[]>(settingId, []));
paths.add(path);
@ -533,15 +540,15 @@ class AddToSkipPathsQuickFixProvider implements vscode.CodeActionProvider {
const fixes: vscode.CodeAction[] = [];
for (const diagnostic of context.diagnostics) {
if (diagnostic instanceof FileDoesNotExistDiagnostic) {
if (diagnostic instanceof LinkDoesNotExistDiagnostic) {
const fix = new vscode.CodeAction(
localize('skipPathsQuickFix.title', "Add '{0}' to paths that skip link validation.", diagnostic.path),
localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", diagnostic.link),
vscode.CodeActionKind.QuickFix);
fix.command = {
command: AddToSkipPathsQuickFixProvider._addToSkipPathsCommandId,
command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
title: '',
arguments: [document.uri, diagnostic.path]
arguments: [document.uri, diagnostic.link]
};
fixes.push(fix);
}
@ -563,5 +570,5 @@ export function register(
return vscode.Disposable.from(
configuration,
manager,
AddToSkipPathsQuickFixProvider.register(selector, commandManager));
AddToIgnoreLinksQuickFixProvider.register(selector, commandManager));
}

View file

@ -26,7 +26,7 @@ async function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents:
validateFilePaths: DiagnosticLevel.warning,
validateOwnHeaders: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
skipPaths: [],
ignoreLinks: [],
}, noopToken)
).diagnostics;
}
@ -44,7 +44,7 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
constructor(
private readonly enabled: boolean = true,
private readonly skipPaths: string[] = [],
private readonly ignoreLinks: string[] = [],
) { }
getOptions(_resource: vscode.Uri): DiagnosticOptions {
@ -54,7 +54,7 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
validateFilePaths: DiagnosticLevel.ignore,
validateOwnHeaders: DiagnosticLevel.ignore,
validateReferences: DiagnosticLevel.ignore,
skipPaths: this.skipPaths,
ignoreLinks: this.ignoreLinks,
};
}
return {
@ -62,7 +62,7 @@ class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
validateFilePaths: DiagnosticLevel.warning,
validateOwnHeaders: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
skipPaths: this.skipPaths,
ignoreLinks: this.ignoreLinks,
};
}
}
@ -196,7 +196,7 @@ suite('markdown: Diagnostics', () => {
assert.deepStrictEqual(diagnostics.length, 0);
});
test('skipPaths should allow skipping non-existent file', async () => {
test('ignoreLinks should allow skipping link to non-existent file', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](/no-such-file#header)`,
));
@ -206,7 +206,7 @@ suite('markdown: Diagnostics', () => {
assert.deepStrictEqual(diagnostics.length, 0);
});
test('skipPaths should not consider link fragment', async () => {
test('ignoreLinks should not consider link fragment', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](/no-such-file#header)`,
));
@ -216,7 +216,7 @@ suite('markdown: Diagnostics', () => {
assert.deepStrictEqual(diagnostics.length, 0);
});
test('skipPaths should support globs', async () => {
test('ignoreLinks should support globs', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](/images/aaa.png)`,
`![i](/images/sub/bbb.png)`,
@ -227,4 +227,33 @@ suite('markdown: Diagnostics', () => {
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
test('ignoreLinks should support ignoring header', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](#no-such)`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['#no-such']));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
test('ignoreLinks should support ignoring header in file', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`![i](/doc2.md#no-such)`,
));
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(''));
const contents = new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]);
{
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md#no-such']));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
}
{
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md#*']));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
}
});
});