mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-19 08:08:39 +01:00
Don't include reference links that are inside other links (#153864)
Fixes #150921
This commit is contained in:
@@ -514,17 +514,17 @@ export class DiagnosticComputer {
|
||||
const diagnostics: vscode.Diagnostic[] = [];
|
||||
for (const link of links) {
|
||||
if (link.href.kind === 'internal'
|
||||
&& link.source.text.startsWith('#')
|
||||
&& link.source.hrefText.startsWith('#')
|
||||
&& link.href.path.toString() === doc.uri.toString()
|
||||
&& link.href.fragment
|
||||
&& !toc.lookup(link.href.fragment)
|
||||
) {
|
||||
if (!this.isIgnoredLink(options, link.source.text)) {
|
||||
if (!this.isIgnoredLink(options, link.source.hrefText)) {
|
||||
diagnostics.push(new LinkDoesNotExistDiagnostic(
|
||||
link.source.hrefRange,
|
||||
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
|
||||
severity,
|
||||
link.source.text));
|
||||
link.source.hrefText));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -556,7 +556,7 @@ export class DiagnosticComputer {
|
||||
const fragmentErrorSeverity = toSeverity(typeof options.validateMarkdownFileLinkFragments === 'undefined' ? options.validateFragmentLinks : options.validateMarkdownFileLinkFragments);
|
||||
|
||||
// We've already validated our own fragment links in `validateOwnHeaderLinks`
|
||||
const linkSet = new FileLinkMap(links.filter(link => !link.source.text.startsWith('#')));
|
||||
const linkSet = new FileLinkMap(links.filter(link => !link.source.hrefText.startsWith('#')));
|
||||
if (linkSet.size === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -585,10 +585,10 @@ export class DiagnosticComputer {
|
||||
if (fragmentLinks.length) {
|
||||
const toc = await this.tocProvider.get(resolvedHrefPath);
|
||||
for (const link of fragmentLinks) {
|
||||
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.text)) {
|
||||
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.hrefText)) {
|
||||
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
|
||||
const range = link.source.fragmentRange?.with({ start: link.source.fragmentRange.start.translate(0, -1) }) ?? link.source.hrefRange;
|
||||
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.text));
|
||||
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.hrefText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,18 +108,34 @@ function getWorkspaceFolder(document: ITextDocument) {
|
||||
}
|
||||
|
||||
export interface MdLinkSource {
|
||||
/**
|
||||
* The full range of the link.
|
||||
*/
|
||||
readonly range: vscode.Range;
|
||||
|
||||
/**
|
||||
* The file where the link is defined.
|
||||
*/
|
||||
readonly resource: vscode.Uri;
|
||||
|
||||
/**
|
||||
* The original text of the link destination in code.
|
||||
*/
|
||||
readonly text: string;
|
||||
readonly hrefText: string;
|
||||
|
||||
/**
|
||||
* The original text of just the link's path in code.
|
||||
*/
|
||||
readonly pathText: string;
|
||||
|
||||
readonly resource: vscode.Uri;
|
||||
/**
|
||||
* The range of the path.
|
||||
*/
|
||||
readonly hrefRange: vscode.Range;
|
||||
|
||||
/**
|
||||
* The range of the fragment within the path.
|
||||
*/
|
||||
readonly fragmentRange: vscode.Range | undefined;
|
||||
}
|
||||
|
||||
@@ -145,32 +161,37 @@ function extractDocumentLink(
|
||||
document: ITextDocument,
|
||||
pre: string,
|
||||
rawLink: string,
|
||||
matchIndex: number | undefined
|
||||
matchIndex: number,
|
||||
fullMatch: string,
|
||||
): MdLink | undefined {
|
||||
const isAngleBracketLink = rawLink.startsWith('<');
|
||||
const link = stripAngleBrackets(rawLink);
|
||||
|
||||
const offset = (matchIndex || 0) + pre.length + (isAngleBracketLink ? 1 : 0);
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
let linkTarget: ExternalHref | InternalHref | undefined;
|
||||
try {
|
||||
const linkTarget = resolveLink(document, link);
|
||||
if (!linkTarget) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
kind: 'link',
|
||||
href: linkTarget,
|
||||
source: {
|
||||
text: link,
|
||||
resource: document.uri,
|
||||
hrefRange: new vscode.Range(linkStart, linkEnd),
|
||||
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
|
||||
}
|
||||
};
|
||||
linkTarget = resolveLink(document, link);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
if (!linkTarget) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const linkStart = document.positionAt(matchIndex);
|
||||
const linkEnd = linkStart.translate(0, fullMatch.length);
|
||||
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
|
||||
const hrefEnd = hrefStart.translate(0, link.length);
|
||||
return {
|
||||
kind: 'link',
|
||||
href: linkTarget,
|
||||
source: {
|
||||
hrefText: link,
|
||||
resource: document.uri,
|
||||
range: new vscode.Range(linkStart, linkEnd),
|
||||
hrefRange: new vscode.Range(hrefStart, hrefEnd),
|
||||
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined {
|
||||
@@ -278,13 +299,28 @@ class NoLinkRanges {
|
||||
/**
|
||||
* Inline code spans where links should not be detected
|
||||
*/
|
||||
public readonly inline: Map</* line number */ number, readonly vscode.Range[]>
|
||||
public readonly inline: Map</* line number */ number, vscode.Range[]>
|
||||
) { }
|
||||
|
||||
contains(position: vscode.Position): boolean {
|
||||
return this.multiline.some(interval => position.line >= interval[0] && position.line < interval[1]) ||
|
||||
!!this.inline.get(position.line)?.some(inlineRange => inlineRange.contains(position));
|
||||
}
|
||||
|
||||
concatInline(inlineRanges: Iterable<vscode.Range>): NoLinkRanges {
|
||||
const newInline = new Map(this.inline);
|
||||
for (const range of inlineRanges) {
|
||||
for (let line = range.start.line; line <= range.end.line; ++line) {
|
||||
let entry = newInline.get(line);
|
||||
if (!entry) {
|
||||
entry = [];
|
||||
newInline.set(line, entry);
|
||||
}
|
||||
entry.push(range);
|
||||
}
|
||||
}
|
||||
return new NoLinkRanges(this.multiline, newInline);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -302,9 +338,10 @@ export class MdLinkComputer {
|
||||
return [];
|
||||
}
|
||||
|
||||
const inlineLinks = Array.from(this.getInlineLinks(document, noLinkRanges));
|
||||
return Array.from([
|
||||
...this.getInlineLinks(document, noLinkRanges),
|
||||
...this.getReferenceLinks(document, noLinkRanges),
|
||||
...inlineLinks,
|
||||
...this.getReferenceLinks(document, noLinkRanges.concatInline(inlineLinks.map(x => x.source.range))),
|
||||
...this.getLinkDefinitions(document, noLinkRanges),
|
||||
...this.getAutoLinks(document, noLinkRanges),
|
||||
]);
|
||||
@@ -313,13 +350,13 @@ export class MdLinkComputer {
|
||||
private *getInlineLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
|
||||
const text = document.getText();
|
||||
for (const match of text.matchAll(linkPattern)) {
|
||||
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index);
|
||||
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index ?? 0, match[0]);
|
||||
if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange.start)) {
|
||||
yield matchLinkData;
|
||||
|
||||
// Also check link destination for links
|
||||
for (const innerMatch of match[1].matchAll(linkPattern)) {
|
||||
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0));
|
||||
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0), innerMatch[0]);
|
||||
if (innerData) {
|
||||
yield innerData;
|
||||
}
|
||||
@@ -328,77 +365,83 @@ export class MdLinkComputer {
|
||||
}
|
||||
}
|
||||
|
||||
private * getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
|
||||
private *getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
|
||||
const text = document.getText();
|
||||
|
||||
for (const match of text.matchAll(autoLinkPattern)) {
|
||||
const linkOffset = (match.index ?? 0);
|
||||
const linkStart = document.positionAt(linkOffset);
|
||||
if (noLinkRanges.contains(linkStart)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const link = match[1];
|
||||
const linkTarget = resolveLink(document, link);
|
||||
if (linkTarget) {
|
||||
const offset = (match.index ?? 0) + 1;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
const hrefRange = new vscode.Range(linkStart, linkEnd);
|
||||
if (noLinkRanges.contains(hrefRange.start)) {
|
||||
continue;
|
||||
}
|
||||
yield {
|
||||
kind: 'link',
|
||||
href: linkTarget,
|
||||
source: {
|
||||
text: link,
|
||||
resource: document.uri,
|
||||
hrefRange: new vscode.Range(linkStart, linkEnd),
|
||||
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
|
||||
}
|
||||
};
|
||||
if (!linkTarget) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const linkEnd = linkStart.translate(0, match[0].length);
|
||||
const hrefStart = linkStart.translate(0, 1);
|
||||
const hrefEnd = hrefStart.translate(0, link.length);
|
||||
yield {
|
||||
kind: 'link',
|
||||
href: linkTarget,
|
||||
source: {
|
||||
hrefText: link,
|
||||
resource: document.uri,
|
||||
hrefRange: new vscode.Range(hrefStart, hrefEnd),
|
||||
range: new vscode.Range(linkStart, linkEnd),
|
||||
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private *getReferenceLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
|
||||
const text = document.getText();
|
||||
for (const match of text.matchAll(referenceLinkPattern)) {
|
||||
let linkStart: vscode.Position;
|
||||
let linkEnd: vscode.Position;
|
||||
const linkStart = document.positionAt(match.index ?? 0);
|
||||
if (noLinkRanges.contains(linkStart)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let hrefStart: vscode.Position;
|
||||
let hrefEnd: vscode.Position;
|
||||
let reference = match[4];
|
||||
if (reference === '') { // [ref][],
|
||||
reference = match[3];
|
||||
const offset = ((match.index ?? 0) + match[1].length) + 1;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + reference.length);
|
||||
hrefStart = document.positionAt(offset);
|
||||
hrefEnd = document.positionAt(offset + reference.length);
|
||||
} else if (reference) { // [text][ref]
|
||||
const pre = match[2];
|
||||
const offset = ((match.index ?? 0) + match[1].length) + pre.length;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + reference.length);
|
||||
hrefStart = document.positionAt(offset);
|
||||
hrefEnd = document.positionAt(offset + reference.length);
|
||||
} else if (match[5]) { // [ref]
|
||||
reference = match[5];
|
||||
const offset = ((match.index ?? 0) + match[1].length) + 1;
|
||||
linkStart = document.positionAt(offset);
|
||||
const line = document.lineAt(linkStart.line);
|
||||
hrefStart = document.positionAt(offset);
|
||||
const line = document.lineAt(hrefStart.line);
|
||||
// See if link looks like a checkbox
|
||||
const checkboxMatch = line.text.match(/^\s*[\-\*]\s*\[x\]/i);
|
||||
if (checkboxMatch && linkStart.character <= checkboxMatch[0].length) {
|
||||
if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) {
|
||||
continue;
|
||||
}
|
||||
linkEnd = document.positionAt(offset + reference.length);
|
||||
hrefEnd = document.positionAt(offset + reference.length);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
const hrefRange = new vscode.Range(linkStart, linkEnd);
|
||||
if (noLinkRanges.contains(hrefRange.start)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const linkEnd = linkStart.translate(0, match[0].length);
|
||||
yield {
|
||||
kind: 'link',
|
||||
source: {
|
||||
text: reference,
|
||||
hrefText: reference,
|
||||
pathText: reference,
|
||||
resource: document.uri,
|
||||
hrefRange,
|
||||
range: new vscode.Range(linkStart, linkEnd),
|
||||
hrefRange: new vscode.Range(hrefStart, hrefEnd),
|
||||
fragmentRange: undefined,
|
||||
},
|
||||
href: {
|
||||
@@ -412,44 +455,41 @@ export class MdLinkComputer {
|
||||
private *getLinkDefinitions(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
|
||||
const text = document.getText();
|
||||
for (const match of text.matchAll(definitionPattern)) {
|
||||
const pre = match[1];
|
||||
const reference = match[2];
|
||||
const link = match[3].trim();
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
|
||||
const refStart = document.positionAt((match.index ?? 0) + 1);
|
||||
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
|
||||
|
||||
let linkStart: vscode.Position;
|
||||
let linkEnd: vscode.Position;
|
||||
let text: string;
|
||||
if (angleBracketLinkRe.test(link)) {
|
||||
linkStart = document.positionAt(offset + 1);
|
||||
linkEnd = document.positionAt(offset + link.length - 1);
|
||||
text = link.substring(1, link.length - 1);
|
||||
} else {
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + link.length);
|
||||
text = link;
|
||||
}
|
||||
const hrefRange = new vscode.Range(linkStart, linkEnd);
|
||||
if (noLinkRanges.contains(hrefRange.start)) {
|
||||
const offset = (match.index ?? 0);
|
||||
const linkStart = document.positionAt(offset);
|
||||
if (noLinkRanges.contains(linkStart)) {
|
||||
continue;
|
||||
}
|
||||
const target = resolveLink(document, text);
|
||||
if (target) {
|
||||
yield {
|
||||
kind: 'definition',
|
||||
source: {
|
||||
text: link,
|
||||
resource: document.uri,
|
||||
hrefRange,
|
||||
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
|
||||
},
|
||||
ref: { text: reference, range: refRange },
|
||||
href: target,
|
||||
};
|
||||
|
||||
const pre = match[1];
|
||||
const reference = match[2];
|
||||
const rawLinkText = match[3].trim();
|
||||
const target = resolveLink(document, rawLinkText);
|
||||
if (!target) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const isAngleBracketLink = angleBracketLinkRe.test(rawLinkText);
|
||||
const linkText = stripAngleBrackets(rawLinkText);
|
||||
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
|
||||
const hrefEnd = hrefStart.translate(0, linkText.length);
|
||||
const hrefRange = new vscode.Range(hrefStart, hrefEnd);
|
||||
|
||||
const refStart = linkStart.translate(0, 1);
|
||||
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
|
||||
const linkEnd = linkStart.translate(0, match[0].length);
|
||||
yield {
|
||||
kind: 'definition',
|
||||
source: {
|
||||
hrefText: linkText,
|
||||
resource: document.uri,
|
||||
range: new vscode.Range(linkStart, linkEnd),
|
||||
hrefRange,
|
||||
...getLinkSourceFragmentInfo(document, rawLinkText, hrefStart, hrefEnd),
|
||||
},
|
||||
ref: { text: reference, range: refRange },
|
||||
href: target,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ export class MdReferencesProvider extends Disposable {
|
||||
}
|
||||
|
||||
// Exclude cases where the file is implicitly referencing itself
|
||||
if (link.source.text.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
|
||||
if (link.source.hrefText.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ export class MdVsCodeRenameProvider extends Disposable implements vscode.RenameP
|
||||
if (ref.kind === 'link') {
|
||||
// Try to preserve style of existing links
|
||||
let newPath: string;
|
||||
if (ref.link.source.text.startsWith('/')) {
|
||||
if (ref.link.source.hrefText.startsWith('/')) {
|
||||
const root = resolveDocumentLink('/', ref.link.source.resource);
|
||||
newPath = '/' + path.relative(root.toString(true), rawNewFilePath.toString(true));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user