Skip to content

Commit e443613

Browse files
authored
Don't include reference links that are inside other links (microsoft#153864)
Fixes microsoft#150921
1 parent a893735 commit e443613

File tree

5 files changed

+170
-118
lines changed

5 files changed

+170
-118
lines changed

extensions/markdown-language-features/src/languageFeatures/diagnostics.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -514,17 +514,17 @@ export class DiagnosticComputer {
514514
const diagnostics: vscode.Diagnostic[] = [];
515515
for (const link of links) {
516516
if (link.href.kind === 'internal'
517-
&& link.source.text.startsWith('#')
517+
&& link.source.hrefText.startsWith('#')
518518
&& link.href.path.toString() === doc.uri.toString()
519519
&& link.href.fragment
520520
&& !toc.lookup(link.href.fragment)
521521
) {
522-
if (!this.isIgnoredLink(options, link.source.text)) {
522+
if (!this.isIgnoredLink(options, link.source.hrefText)) {
523523
diagnostics.push(new LinkDoesNotExistDiagnostic(
524524
link.source.hrefRange,
525525
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
526526
severity,
527-
link.source.text));
527+
link.source.hrefText));
528528
}
529529
}
530530
}
@@ -556,7 +556,7 @@ export class DiagnosticComputer {
556556
const fragmentErrorSeverity = toSeverity(typeof options.validateMarkdownFileLinkFragments === 'undefined' ? options.validateFragmentLinks : options.validateMarkdownFileLinkFragments);
557557

558558
// We've already validated our own fragment links in `validateOwnHeaderLinks`
559-
const linkSet = new FileLinkMap(links.filter(link => !link.source.text.startsWith('#')));
559+
const linkSet = new FileLinkMap(links.filter(link => !link.source.hrefText.startsWith('#')));
560560
if (linkSet.size === 0) {
561561
return [];
562562
}
@@ -585,10 +585,10 @@ export class DiagnosticComputer {
585585
if (fragmentLinks.length) {
586586
const toc = await this.tocProvider.get(resolvedHrefPath);
587587
for (const link of fragmentLinks) {
588-
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.text)) {
588+
if (!toc.lookup(link.fragment) && !this.isIgnoredLink(options, link.source.pathText) && !this.isIgnoredLink(options, link.source.hrefText)) {
589589
const msg = localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', link.fragment);
590590
const range = link.source.fragmentRange?.with({ start: link.source.fragmentRange.start.translate(0, -1) }) ?? link.source.hrefRange;
591-
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.text));
591+
diagnostics.push(new LinkDoesNotExistDiagnostic(range, msg, fragmentErrorSeverity, link.source.hrefText));
592592
}
593593
}
594594
}

extensions/markdown-language-features/src/languageFeatures/documentLinks.ts

Lines changed: 136 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -108,18 +108,34 @@ function getWorkspaceFolder(document: ITextDocument) {
108108
}
109109

110110
export interface MdLinkSource {
111+
/**
112+
* The full range of the link.
113+
*/
114+
readonly range: vscode.Range;
115+
116+
/**
117+
* The file where the link is defined.
118+
*/
119+
readonly resource: vscode.Uri;
120+
111121
/**
112122
* The original text of the link destination in code.
113123
*/
114-
readonly text: string;
124+
readonly hrefText: string;
115125

116126
/**
117127
* The original text of just the link's path in code.
118128
*/
119129
readonly pathText: string;
120130

121-
readonly resource: vscode.Uri;
131+
/**
132+
* The range of the path.
133+
*/
122134
readonly hrefRange: vscode.Range;
135+
136+
/**
137+
* The range of the fragment within the path.
138+
*/
123139
readonly fragmentRange: vscode.Range | undefined;
124140
}
125141

@@ -145,32 +161,37 @@ function extractDocumentLink(
145161
document: ITextDocument,
146162
pre: string,
147163
rawLink: string,
148-
matchIndex: number | undefined
164+
matchIndex: number,
165+
fullMatch: string,
149166
): MdLink | undefined {
150167
const isAngleBracketLink = rawLink.startsWith('<');
151168
const link = stripAngleBrackets(rawLink);
152169

153-
const offset = (matchIndex || 0) + pre.length + (isAngleBracketLink ? 1 : 0);
154-
const linkStart = document.positionAt(offset);
155-
const linkEnd = document.positionAt(offset + link.length);
170+
let linkTarget: ExternalHref | InternalHref | undefined;
156171
try {
157-
const linkTarget = resolveLink(document, link);
158-
if (!linkTarget) {
159-
return undefined;
160-
}
161-
return {
162-
kind: 'link',
163-
href: linkTarget,
164-
source: {
165-
text: link,
166-
resource: document.uri,
167-
hrefRange: new vscode.Range(linkStart, linkEnd),
168-
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
169-
}
170-
};
172+
linkTarget = resolveLink(document, link);
171173
} catch {
172174
return undefined;
173175
}
176+
if (!linkTarget) {
177+
return undefined;
178+
}
179+
180+
const linkStart = document.positionAt(matchIndex);
181+
const linkEnd = linkStart.translate(0, fullMatch.length);
182+
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
183+
const hrefEnd = hrefStart.translate(0, link.length);
184+
return {
185+
kind: 'link',
186+
href: linkTarget,
187+
source: {
188+
hrefText: link,
189+
resource: document.uri,
190+
range: new vscode.Range(linkStart, linkEnd),
191+
hrefRange: new vscode.Range(hrefStart, hrefEnd),
192+
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
193+
}
194+
};
174195
}
175196

176197
function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined {
@@ -278,13 +299,28 @@ class NoLinkRanges {
278299
/**
279300
* Inline code spans where links should not be detected
280301
*/
281-
public readonly inline: Map</* line number */ number, readonly vscode.Range[]>
302+
public readonly inline: Map</* line number */ number, vscode.Range[]>
282303
) { }
283304

284305
contains(position: vscode.Position): boolean {
285306
return this.multiline.some(interval => position.line >= interval[0] && position.line < interval[1]) ||
286307
!!this.inline.get(position.line)?.some(inlineRange => inlineRange.contains(position));
287308
}
309+
310+
concatInline(inlineRanges: Iterable<vscode.Range>): NoLinkRanges {
311+
const newInline = new Map(this.inline);
312+
for (const range of inlineRanges) {
313+
for (let line = range.start.line; line <= range.end.line; ++line) {
314+
let entry = newInline.get(line);
315+
if (!entry) {
316+
entry = [];
317+
newInline.set(line, entry);
318+
}
319+
entry.push(range);
320+
}
321+
}
322+
return new NoLinkRanges(this.multiline, newInline);
323+
}
288324
}
289325

290326
/**
@@ -302,9 +338,10 @@ export class MdLinkComputer {
302338
return [];
303339
}
304340

341+
const inlineLinks = Array.from(this.getInlineLinks(document, noLinkRanges));
305342
return Array.from([
306-
...this.getInlineLinks(document, noLinkRanges),
307-
...this.getReferenceLinks(document, noLinkRanges),
343+
...inlineLinks,
344+
...this.getReferenceLinks(document, noLinkRanges.concatInline(inlineLinks.map(x => x.source.range))),
308345
...this.getLinkDefinitions(document, noLinkRanges),
309346
...this.getAutoLinks(document, noLinkRanges),
310347
]);
@@ -313,13 +350,13 @@ export class MdLinkComputer {
313350
private *getInlineLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
314351
const text = document.getText();
315352
for (const match of text.matchAll(linkPattern)) {
316-
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index);
353+
const matchLinkData = extractDocumentLink(document, match[1], match[2], match.index ?? 0, match[0]);
317354
if (matchLinkData && !noLinkRanges.contains(matchLinkData.source.hrefRange.start)) {
318355
yield matchLinkData;
319356

320357
// Also check link destination for links
321358
for (const innerMatch of match[1].matchAll(linkPattern)) {
322-
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0));
359+
const innerData = extractDocumentLink(document, innerMatch[1], innerMatch[2], (match.index ?? 0) + (innerMatch.index ?? 0), innerMatch[0]);
323360
if (innerData) {
324361
yield innerData;
325362
}
@@ -328,77 +365,83 @@ export class MdLinkComputer {
328365
}
329366
}
330367

331-
private * getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
368+
private *getAutoLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
332369
const text = document.getText();
333-
334370
for (const match of text.matchAll(autoLinkPattern)) {
371+
const linkOffset = (match.index ?? 0);
372+
const linkStart = document.positionAt(linkOffset);
373+
if (noLinkRanges.contains(linkStart)) {
374+
continue;
375+
}
376+
335377
const link = match[1];
336378
const linkTarget = resolveLink(document, link);
337-
if (linkTarget) {
338-
const offset = (match.index ?? 0) + 1;
339-
const linkStart = document.positionAt(offset);
340-
const linkEnd = document.positionAt(offset + link.length);
341-
const hrefRange = new vscode.Range(linkStart, linkEnd);
342-
if (noLinkRanges.contains(hrefRange.start)) {
343-
continue;
344-
}
345-
yield {
346-
kind: 'link',
347-
href: linkTarget,
348-
source: {
349-
text: link,
350-
resource: document.uri,
351-
hrefRange: new vscode.Range(linkStart, linkEnd),
352-
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
353-
}
354-
};
379+
if (!linkTarget) {
380+
continue;
355381
}
382+
383+
const linkEnd = linkStart.translate(0, match[0].length);
384+
const hrefStart = linkStart.translate(0, 1);
385+
const hrefEnd = hrefStart.translate(0, link.length);
386+
yield {
387+
kind: 'link',
388+
href: linkTarget,
389+
source: {
390+
hrefText: link,
391+
resource: document.uri,
392+
hrefRange: new vscode.Range(hrefStart, hrefEnd),
393+
range: new vscode.Range(linkStart, linkEnd),
394+
...getLinkSourceFragmentInfo(document, link, hrefStart, hrefEnd),
395+
}
396+
};
356397
}
357398
}
358399

359400
private *getReferenceLinks(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLink> {
360401
const text = document.getText();
361402
for (const match of text.matchAll(referenceLinkPattern)) {
362-
let linkStart: vscode.Position;
363-
let linkEnd: vscode.Position;
403+
const linkStart = document.positionAt(match.index ?? 0);
404+
if (noLinkRanges.contains(linkStart)) {
405+
continue;
406+
}
407+
408+
let hrefStart: vscode.Position;
409+
let hrefEnd: vscode.Position;
364410
let reference = match[4];
365411
if (reference === '') { // [ref][],
366412
reference = match[3];
367413
const offset = ((match.index ?? 0) + match[1].length) + 1;
368-
linkStart = document.positionAt(offset);
369-
linkEnd = document.positionAt(offset + reference.length);
414+
hrefStart = document.positionAt(offset);
415+
hrefEnd = document.positionAt(offset + reference.length);
370416
} else if (reference) { // [text][ref]
371417
const pre = match[2];
372418
const offset = ((match.index ?? 0) + match[1].length) + pre.length;
373-
linkStart = document.positionAt(offset);
374-
linkEnd = document.positionAt(offset + reference.length);
419+
hrefStart = document.positionAt(offset);
420+
hrefEnd = document.positionAt(offset + reference.length);
375421
} else if (match[5]) { // [ref]
376422
reference = match[5];
377423
const offset = ((match.index ?? 0) + match[1].length) + 1;
378-
linkStart = document.positionAt(offset);
379-
const line = document.lineAt(linkStart.line);
424+
hrefStart = document.positionAt(offset);
425+
const line = document.lineAt(hrefStart.line);
380426
// See if link looks like a checkbox
381427
const checkboxMatch = line.text.match(/^\s*[\-\*]\s*\[x\]/i);
382-
if (checkboxMatch && linkStart.character <= checkboxMatch[0].length) {
428+
if (checkboxMatch && hrefStart.character <= checkboxMatch[0].length) {
383429
continue;
384430
}
385-
linkEnd = document.positionAt(offset + reference.length);
431+
hrefEnd = document.positionAt(offset + reference.length);
386432
} else {
387433
continue;
388434
}
389435

390-
const hrefRange = new vscode.Range(linkStart, linkEnd);
391-
if (noLinkRanges.contains(hrefRange.start)) {
392-
continue;
393-
}
394-
436+
const linkEnd = linkStart.translate(0, match[0].length);
395437
yield {
396438
kind: 'link',
397439
source: {
398-
text: reference,
440+
hrefText: reference,
399441
pathText: reference,
400442
resource: document.uri,
401-
hrefRange,
443+
range: new vscode.Range(linkStart, linkEnd),
444+
hrefRange: new vscode.Range(hrefStart, hrefEnd),
402445
fragmentRange: undefined,
403446
},
404447
href: {
@@ -412,44 +455,41 @@ export class MdLinkComputer {
412455
private *getLinkDefinitions(document: ITextDocument, noLinkRanges: NoLinkRanges): Iterable<MdLinkDefinition> {
413456
const text = document.getText();
414457
for (const match of text.matchAll(definitionPattern)) {
458+
const offset = (match.index ?? 0);
459+
const linkStart = document.positionAt(offset);
460+
if (noLinkRanges.contains(linkStart)) {
461+
continue;
462+
}
463+
415464
const pre = match[1];
416465
const reference = match[2];
417-
const link = match[3].trim();
418-
const offset = (match.index || 0) + pre.length;
419-
420-
const refStart = document.positionAt((match.index ?? 0) + 1);
421-
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
422-
423-
let linkStart: vscode.Position;
424-
let linkEnd: vscode.Position;
425-
let text: string;
426-
if (angleBracketLinkRe.test(link)) {
427-
linkStart = document.positionAt(offset + 1);
428-
linkEnd = document.positionAt(offset + link.length - 1);
429-
text = link.substring(1, link.length - 1);
430-
} else {
431-
linkStart = document.positionAt(offset);
432-
linkEnd = document.positionAt(offset + link.length);
433-
text = link;
434-
}
435-
const hrefRange = new vscode.Range(linkStart, linkEnd);
436-
if (noLinkRanges.contains(hrefRange.start)) {
466+
const rawLinkText = match[3].trim();
467+
const target = resolveLink(document, rawLinkText);
468+
if (!target) {
437469
continue;
438470
}
439-
const target = resolveLink(document, text);
440-
if (target) {
441-
yield {
442-
kind: 'definition',
443-
source: {
444-
text: link,
445-
resource: document.uri,
446-
hrefRange,
447-
...getLinkSourceFragmentInfo(document, link, linkStart, linkEnd),
448-
},
449-
ref: { text: reference, range: refRange },
450-
href: target,
451-
};
452-
}
471+
472+
const isAngleBracketLink = angleBracketLinkRe.test(rawLinkText);
473+
const linkText = stripAngleBrackets(rawLinkText);
474+
const hrefStart = linkStart.translate(0, pre.length + (isAngleBracketLink ? 1 : 0));
475+
const hrefEnd = hrefStart.translate(0, linkText.length);
476+
const hrefRange = new vscode.Range(hrefStart, hrefEnd);
477+
478+
const refStart = linkStart.translate(0, 1);
479+
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
480+
const linkEnd = linkStart.translate(0, match[0].length);
481+
yield {
482+
kind: 'definition',
483+
source: {
484+
hrefText: linkText,
485+
resource: document.uri,
486+
range: new vscode.Range(linkStart, linkEnd),
487+
hrefRange,
488+
...getLinkSourceFragmentInfo(document, rawLinkText, hrefStart, hrefEnd),
489+
},
490+
ref: { text: reference, range: refRange },
491+
href: target,
492+
};
453493
}
454494
}
455495
}

0 commit comments

Comments
 (0)