Add skipPaths option for markdown link validation (#149859)

The new `markdown.experimental.validate.fileLinks.skipPaths` setting lets you specify a list of paths (as globs) that should not be validation

This is useful since markdown is used in a range of environments, and sometimes you may need to link to paths that don't exist on disk but will exist on deployment

A few other changes here:

- Adds a quick fix that adds paths to `skipPaths`
- Rename existing settings to use the `.enabled` prefix
This commit is contained in:
Matt Bierner
2022-05-18 14:37:08 -07:00
committed by GitHub
parent fd3a84fdfc
commit daf0d5e551
7 changed files with 194 additions and 20 deletions

View File

@@ -5,6 +5,7 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as picomatch from 'picomatch';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { Delayer } from '../util/async';
@@ -14,6 +15,7 @@ import { Limiter } from '../util/limiter';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider, MdLinkSource } from './documentLinkProvider';
import { tryFindMdDocumentForLink } from './references';
import { CommandManager } from '../commandManager';
const localize = nls.loadMessageBundle();
@@ -37,6 +39,7 @@ export interface DiagnosticOptions {
readonly validateReferences: DiagnosticLevel;
readonly validateOwnHeaders: DiagnosticLevel;
readonly validateFilePaths: DiagnosticLevel;
readonly skipPaths: readonly string[];
}
function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined {
@@ -56,7 +59,13 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf
super();
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('markdown.experimental.validate.enabled')) {
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.fileLinks.enabled')
|| e.affectsConfiguration('markdown.experimental.validate.fileLinks.skipPaths')
) {
this._onDidChange.fire();
}
}));
@@ -66,9 +75,10 @@ class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConf
const config = vscode.workspace.getConfiguration('markdown', resource);
return {
enabled: config.get<boolean>('experimental.validate.enabled', false),
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks', DiagnosticLevel.ignore),
validateOwnHeaders: config.get<DiagnosticLevel>('experimental.validate.headerLinks', DiagnosticLevel.ignore),
validateFilePaths: config.get<DiagnosticLevel>('experimental.validate.fileLinks', DiagnosticLevel.ignore),
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', []),
};
}
}
@@ -206,6 +216,16 @@ class LinkWatcher extends Disposable {
}
}
class FileDoesNotExistDiagnostic extends vscode.Diagnostic {
public readonly path: string;
constructor(range: vscode.Range, message: string, severity: vscode.DiagnosticSeverity, path: string) {
super(range, message, severity);
this.path = path;
}
}
export class DiagnosticManager extends Disposable {
private readonly collection: vscode.DiagnosticCollection;
@@ -459,9 +479,11 @@ export class DiagnosticComputer {
}
if (!hrefDoc && !await this.workspaceContents.pathExists(path)) {
const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.toString(true));
const msg = localize('invalidPathLink', 'File does not exist at path: {0}', path.fsPath);
for (const link of links) {
diagnostics.push(new vscode.Diagnostic(link.source.hrefRange, msg, severity));
if (!options.skipPaths.some(glob => picomatch.isMatch(link.source.pathText, glob))) {
diagnostics.push(new FileDoesNotExistDiagnostic(link.source.hrefRange, msg, severity, link.source.pathText));
}
}
} else if (hrefDoc) {
// Validate each of the links to headers in the file
@@ -482,12 +504,64 @@ export class DiagnosticComputer {
}
}
class AddToSkipPathsQuickFixProvider implements vscode.CodeActionProvider {
private static readonly _addToSkipPathsCommandId = '_markdown.addToSkipPaths';
private 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 AddToSkipPathsQuickFixProvider(), AddToSkipPathsQuickFixProvider.metadata);
const commandReg = commandManager.register({
id: AddToSkipPathsQuickFixProvider._addToSkipPathsCommandId,
execute(resource: vscode.Uri, path: string) {
const settingId = 'experimental.validate.fileLinks.skipPaths';
const config = vscode.workspace.getConfiguration('markdown', resource);
const paths = new Set(config.get<string[]>(settingId, []));
paths.add(path);
config.update(settingId, [...paths], vscode.ConfigurationTarget.WorkspaceFolder);
}
});
return vscode.Disposable.from(reg, commandReg);
}
provideCodeActions(document: vscode.TextDocument, _range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, _token: vscode.CancellationToken): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
const fixes: vscode.CodeAction[] = [];
for (const diagnostic of context.diagnostics) {
if (diagnostic instanceof FileDoesNotExistDiagnostic) {
const fix = new vscode.CodeAction(
localize('skipPathsQuickFix.title', "Add '{0}' to paths that skip link validation.", diagnostic.path),
vscode.CodeActionKind.QuickFix);
fix.command = {
command: AddToSkipPathsQuickFixProvider._addToSkipPathsCommandId,
title: '',
arguments: [document.uri, diagnostic.path]
};
fixes.push(fix);
}
}
return fixes;
}
}
export function register(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
workspaceContents: MdWorkspaceContents,
linkProvider: MdLinkProvider,
commandManager: CommandManager,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
return vscode.Disposable.from(configuration, manager);
return vscode.Disposable.from(
configuration,
manager,
AddToSkipPathsQuickFixProvider.register(selector, commandManager));
}

View File

@@ -93,7 +93,16 @@ function getWorkspaceFolder(document: SkinnyTextDocument) {
}
export interface MdLinkSource {
/**
* The original text of the link destination in code.
*/
readonly text: string;
/**
* The original text of just the link's path in code.
*/
readonly pathText: string;
readonly resource: vscode.Uri;
readonly hrefRange: vscode.Range;
readonly fragmentRange: vscode.Range | undefined;
@@ -138,7 +147,7 @@ function extractDocumentLink(
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
}
};
} catch {
@@ -154,6 +163,14 @@ function getFragmentRange(text: string, start: vscode.Position, end: vscode.Posi
return new vscode.Range(start.translate({ characterDelta: index + 1 }), end);
}
function getLinkSourceFragmentInfo(document: SkinnyTextDocument, link: string, linkStart: vscode.Position, linkEnd: vscode.Position): { fragmentRange: vscode.Range | undefined; pathText: string } {
const fragmentRange = getFragmentRange(link, linkStart, linkEnd);
return {
pathText: document.getText(new vscode.Range(linkStart, fragmentRange ? fragmentRange.start.translate(0, -1) : linkEnd)),
fragmentRange,
};
}
const angleBracketLinkRe = /^<(.*)>$/;
/**
@@ -314,7 +331,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
}
};
}
@@ -350,6 +367,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
kind: 'link',
source: {
text: reference,
pathText: reference,
resource: document.uri,
hrefRange,
fragmentRange: undefined,
@@ -402,7 +420,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
text: link,
resource: document.uri,
hrefRange,
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
},
ref: { text: reference, range: refRange },
href: target,