mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-19 16:18:58 +01:00
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:
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user