Clarify markdown validate settings (#151997)

Clairify markdown validate settings

Fixes #150949

- Rename headerLinks -> fragmentLinks
- Add new `fileLink.markdownFragmentsLinks` to validate the headers on fragment links (inherits the default setting value from `fragmentLinks`)
This commit is contained in:
Matt Bierner 2022-06-13 17:06:28 -07:00 committed by GitHub
parent af754ef399
commit 1a07fd15d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 101 additions and 49 deletions

View file

@ -447,10 +447,10 @@
"experimental"
]
},
"markdown.experimental.validate.headerLinks.enabled": {
"markdown.experimental.validate.fragmentLinks.enabled": {
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.experimental.validate.headerLinks.enabled.description%",
"markdownDescription": "%configuration.markdown.experimental.validate.fragmentLinks.enabled.description%",
"default": "warning",
"enum": [
"ignore",
@ -475,6 +475,19 @@
"experimental"
]
},
"markdown.experimental.validate.fileLinks.markdownFragmentLinks": {
"type": "string",
"scope": "resource",
"markdownDescription": "%configuration.markdown.experimental.validate.fileLinks.markdownFragmentLinks.description%",
"enum": [
"ignore",
"warning",
"error"
],
"tags": [
"experimental"
]
},
"markdown.experimental.validate.ignoreLinks": {
"type": "array",
"scope": "resource",

View file

@ -32,8 +32,9 @@
"configuration.markdown.editor.pasteLinks.enabled": "Enable/disable pasting files into a Markdown editor inserts Markdown links.",
"configuration.markdown.experimental.validate.enabled.description": "Enable/disable all error reporting in Markdown files.",
"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.fragmentLinks.enabled.description": "Validate fragment links to headers in the current Markdown file, 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.markdownFragmentLinks.description": "Validate the fragment part of links to headers in other files in Markdown files, e.g. `[link](/path/to/file.md#header)`. Inherits the setting value from `#markdown.experimental.validate.fragmentLinks.enabled#` by default.",
"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

@ -37,17 +37,19 @@ export enum DiagnosticLevel {
export interface DiagnosticOptions {
readonly enabled: boolean;
readonly validateReferences: DiagnosticLevel;
readonly validateOwnHeaders: DiagnosticLevel;
readonly validateFilePaths: DiagnosticLevel;
readonly validateReferences: DiagnosticLevel | undefined;
readonly validateFragmentLinks: DiagnosticLevel | undefined;
readonly validateFileLinks: DiagnosticLevel | undefined;
readonly validateMarkdownFileLinkFragments: DiagnosticLevel | undefined;
readonly ignoreLinks: readonly string[];
}
function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined {
function toSeverity(level: DiagnosticLevel | undefined): vscode.DiagnosticSeverity | undefined {
switch (level) {
case DiagnosticLevel.error: return vscode.DiagnosticSeverity.Error;
case DiagnosticLevel.warning: return vscode.DiagnosticSeverity.Warning;
case DiagnosticLevel.ignore: return undefined;
case undefined: return undefined;
}
}
@ -63,8 +65,9 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf
if (
e.affectsConfiguration('markdown.experimental.validate.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.referenceLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.headerLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fragmentLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.markdownFragmentLinks')
|| e.affectsConfiguration('markdown.experimental.validate.ignoreLinks')
) {
this._onDidChange.fire();
@ -74,11 +77,13 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf
public getOptions(resource: vscode.Uri): DiagnosticOptions {
const config = vscode.workspace.getConfiguration('markdown', resource);
const validateFragmentLinks = config.get<DiagnosticLevel>('experimental.validate.fragmentLinks.enabled');
return {
enabled: config.get<boolean>('experimental.validate.enabled', false),
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),
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks.enabled'),
validateFragmentLinks,
validateFileLinks: config.get<DiagnosticLevel>('experimental.validate.fileLinks.enabled'),
validateMarkdownFileLinkFragments: config.get<DiagnosticLevel | undefined>('markdown.experimental.validate.fileLinks.markdownFragmentLinks', validateFragmentLinks),
ignoreLinks: config.get('experimental.validate.ignoreLinks', []),
};
}
@ -294,7 +299,7 @@ export class DiagnosticManager extends Disposable {
if (doc) {
this.inFlightDiagnostics.trigger(doc.uri, async (token) => {
const state = await this.recomputeDiagnosticState(doc, token);
this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFilePaths ? state.links : []);
this.linkWatcher.updateLinksForDocument(doc.uri, state.config.enabled && state.config.validateFileLinks ? state.links : []);
this.collection.set(doc.uri, state.diagnostics);
});
}
@ -395,13 +400,13 @@ export class DiagnosticComputer {
diagnostics: (await Promise.all([
this.validateFileLinks(doc, options, links, token),
Array.from(this.validateReferenceLinks(options, links)),
this.validateOwnHeaderLinks(doc, options, links, token),
this.validateFragmentLinks(doc, options, links, token),
])).flat()
};
}
private async validateOwnHeaderLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateOwnHeaders);
private async validateFragmentLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateFragmentLinks);
if (typeof severity === 'undefined') {
return [];
}
@ -449,10 +454,11 @@ export class DiagnosticComputer {
}
private async validateFileLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateFilePaths);
if (typeof severity === 'undefined') {
const pathErrorSeverity = toSeverity(options.validateFileLinks);
if (typeof pathErrorSeverity === 'undefined') {
return [];
}
const fragmentErrorSeverity = toSeverity(typeof options.validateMarkdownFileLinkFragments === 'undefined' ? options.validateFragmentLinks : options.validateMarkdownFileLinkFragments);
const linkSet = new FileLinkMap(links);
if (linkSet.size === 0) {
@ -479,10 +485,10 @@ export class DiagnosticComputer {
const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.fsPath);
for (const link of links) {
if (!this.isIgnoredLink(options, link.source.pathText)) {
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.pathText));
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, pathErrorSeverity, link.source.pathText));
}
}
} else if (hrefDoc) {
} else if (hrefDoc && typeof fragmentErrorSeverity !== 'undefined') {
// Validate each of the links to headers in the file
const fragmentLinks = links.filter(x => x.fragment);
if (fragmentLinks.length) {
@ -490,7 +496,7 @@ export class DiagnosticComputer {
for (const link of fragmentLinks) {
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.text)) {
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.text));
diagnostics.push(new LinkDoesNotExistDiagnostic(link.source.hrefRange, msg, fragmentErrorSeverity, link.source.text));
}
}
}

View file

@ -23,15 +23,16 @@ async function getComputedDiagnostics(doc: InMemoryDocument, workspaceContents:
return (
await computer.getDiagnostics(doc, {
enabled: true,
validateFilePaths: DiagnosticLevel.warning,
validateOwnHeaders: DiagnosticLevel.warning,
validateFileLinks: DiagnosticLevel.warning,
validateFragmentLinks: DiagnosticLevel.warning,
validateMarkdownFileLinkFragments: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
ignoreLinks: [],
}, noopToken)
).diagnostics;
}
function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration()) {
function createDiagnosticsManager(workspaceContents: MdWorkspaceContents, configuration = new MemoryDiagnosticConfiguration({})) {
const engine = createNewMarkdownEngine();
const linkProvider = new MdLinkProvider(engine);
return new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
@ -45,32 +46,28 @@ function assertDiagnosticsEqual(actual: readonly vscode.Diagnostic[], expectedRa
}
}
const defaultDiagnosticsOptions = Object.freeze<DiagnosticOptions>({
enabled: true,
validateFileLinks: DiagnosticLevel.warning,
validateMarkdownFileLinkFragments: undefined,
validateFragmentLinks: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
ignoreLinks: [],
});
class MemoryDiagnosticConfiguration implements DiagnosticConfiguration {
private readonly _onDidChange = new vscode.EventEmitter<void>();
public readonly onDidChange = this._onDidChange.event;
constructor(
private readonly enabled: boolean = true,
private readonly ignoreLinks: string[] = [],
private readonly _options: Partial<DiagnosticOptions>,
) { }
getOptions(_resource: vscode.Uri): DiagnosticOptions {
if (!this.enabled) {
return {
enabled: false,
validateFilePaths: DiagnosticLevel.ignore,
validateOwnHeaders: DiagnosticLevel.ignore,
validateReferences: DiagnosticLevel.ignore,
ignoreLinks: this.ignoreLinks,
};
}
return {
enabled: true,
validateFilePaths: DiagnosticLevel.warning,
validateOwnHeaders: DiagnosticLevel.warning,
validateReferences: DiagnosticLevel.warning,
ignoreLinks: this.ignoreLinks,
...defaultDiagnosticsOptions,
...this._options,
};
}
}
@ -172,7 +169,7 @@ suite('markdown: Diagnostics', () => {
`[text][no-such-ref]`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(false));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration({ enabled: false }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
@ -203,17 +200,52 @@ suite('markdown: Diagnostics', () => {
`[text]: /no-such-file`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/no-such-file']));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration({ ignoreLinks: ['/no-such-file'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
test('Should be able to disable fragment validation for external files', 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({ validateMarkdownFileLinkFragments: DiagnosticLevel.ignore }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
test('Disabling own fragment validation should also disable path fragment validation by default', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[b](#no-head)`,
`![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({ validateFragmentLinks: DiagnosticLevel.ignore }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
}
{
// But we should be able to override the default
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration({ validateFragmentLinks: DiagnosticLevel.ignore, validateMarkdownFileLinkFragments: DiagnosticLevel.warning }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 1);
}
});
test('ignoreLinks should allow skipping link to non-existent file', async () => {
const doc1 = new InMemoryDocument(workspacePath('doc1.md'), joinLines(
`[text](/no-such-file#header)`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/no-such-file']));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration({ ignoreLinks: ['/no-such-file'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
@ -223,7 +255,7 @@ suite('markdown: Diagnostics', () => {
`[text](/no-such-file#header)`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/no-such-file']));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration({ ignoreLinks: ['/no-such-file'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
@ -235,7 +267,7 @@ suite('markdown: Diagnostics', () => {
`![i](/images/sub/sub2/ccc.png)`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['/images/**/*.png']));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration({ ignoreLinks: ['/images/**/*.png'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
@ -245,7 +277,7 @@ suite('markdown: Diagnostics', () => {
`![i](#no-such)`,
));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration(true, ['#no-such']));
const manager = createDiagnosticsManager(new InMemoryWorkspaceMarkdownDocuments([doc1]), new MemoryDiagnosticConfiguration({ ignoreLinks: ['#no-such'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
@ -258,12 +290,12 @@ suite('markdown: Diagnostics', () => {
const contents = new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]);
{
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md#no-such']));
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration({ ignoreLinks: ['/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 manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration({ ignoreLinks: ['/doc2.md#*'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
}
@ -276,7 +308,7 @@ suite('markdown: Diagnostics', () => {
const doc2 = new InMemoryDocument(workspacePath('doc2.md'), joinLines(''));
const contents = new InMemoryWorkspaceMarkdownDocuments([doc1, doc2]);
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md']));
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration({ ignoreLinks: ['/doc2.md'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});
@ -289,7 +321,7 @@ suite('markdown: Diagnostics', () => {
));
const contents = new InMemoryWorkspaceMarkdownDocuments([doc1]);
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration(true, ['/doc2.md']));
const manager = createDiagnosticsManager(contents, new MemoryDiagnosticConfiguration({ ignoreLinks: ['/doc2.md'] }));
const { diagnostics } = await manager.recomputeDiagnosticState(doc1, noopToken);
assert.deepStrictEqual(diagnostics.length, 0);
});