Enable find all references and rename of auto links and http(s) links in markdown

For #146777, #146277
This commit is contained in:
Matt Bierner 2022-04-05 15:17:48 -07:00
parent c80acef8a1
commit a56c9f10b7
No known key found for this signature in database
GPG key ID: 099C331567E11888
6 changed files with 158 additions and 7 deletions

View file

@ -170,6 +170,11 @@ const linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*
*/
const referenceLinkPattern = /(?:(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]|\[\s*?([^\s\]]*?)\])(?![\:\(])/g;
/**
* Matches `<http://example.com>`
*/
const autoLinkPattern = /\<(\w+:[^\>\s]+)\>/g;
/**
* Matches `[text]: link`
*/
@ -256,6 +261,7 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
...(await this.getInlineLinks(document)),
...this.getReferenceLinks(document),
...this.getLinkDefinitions(document),
...this.getAutoLinks(document),
]);
}
@ -277,6 +283,30 @@ export class MdLinkProvider implements vscode.DocumentLinkProvider {
return results;
}
private *getAutoLinks(document: SkinnyTextDocument): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(autoLinkPattern)) {
const link = match[1];
const linkTarget = parseLink(document, link);
if (linkTarget) {
const offset = (match.index ?? 0) + 1;
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
yield {
kind: 'link',
href: linkTarget,
source: {
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
}
};
}
}
}
private *getReferenceLinks(document: SkinnyTextDocument): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(referenceLinkPattern)) {

View file

@ -158,8 +158,22 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
return Array.from(this.getReferencesToLinkReference(allLinksInWorkspace, sourceLink.href.ref, { resource: sourceLink.source.resource, range: sourceLink.source.hrefRange }));
}
if (sourceLink.href.kind !== 'internal') {
return [];
if (sourceLink.href.kind === 'external') {
const references: MdReference[] = [];
for (const link of allLinksInWorkspace) {
if (link.href.kind === 'external' && link.href.uri.scheme === sourceLink.href.uri.scheme && link.href.uri.path === sourceLink.href.uri.path) {
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
return references;
}
let targetDoc = await this.workspaceContents.getMarkdownDocument(sourceLink.href.path);
@ -190,9 +204,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
headerTextLocation: entry.headerTextLocation
});
}
}
if (sourceLink.href.fragment) {
for (const link of allLinksInWorkspace) {
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, targetDoc.uri)) {
continue;
@ -226,7 +238,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
return Array.from(this.findAllLinksToFile(resource, allLinksInWorkspace, undefined));
}
private *findAllLinksToFile(resource: vscode.Uri, allLinksInWorkspace: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> {
private * findAllLinksToFile(resource: vscode.Uri, allLinksInWorkspace: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> {
for (const link of allLinksInWorkspace) {
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resource)) {
continue;
@ -248,7 +260,7 @@ export class MdReferencesProvider extends Disposable implements vscode.Reference
}
}
private *getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> {
private * getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> {
for (const link of allLinks) {
let ref: string;
if (link.kind === 'definition') {

View file

@ -52,6 +52,10 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
}
}
if (triggerRef.link.href.kind === 'external') {
return { range: triggerRef.link.source.hrefRange, placeholder: document.getText(triggerRef.link.source.hrefRange) };
}
const { fragmentRange } = triggerRef.link.source;
if (fragmentRange) {
const declaration = this.findHeaderDeclaration(allRefsInfo.references);
@ -98,7 +102,7 @@ export class MdRenameProvider extends Disposable implements vscode.RenameProvide
continue;
}
}
edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, isRefRename && !ref.link.source.fragmentRange ? newName : slug);
edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, isRefRename && !ref.link.source.fragmentRange || ref.link.href.kind === 'external' ? newName : slug);
break;
}
}

View file

@ -239,4 +239,12 @@ suite('markdown.DocumentLinkProvider', () => {
const links = await getLinksForFile(text);
assert.strictEqual(links.length, 1);
});
test('Should find autolinks', async () => {
const links = await getLinksForFile('pre <http://example.com> post');
assert.strictEqual(links.length, 1);
const link = links[0];
assertRangeEqual(link.range, new vscode.Range(0, 5, 0, 23));
});
});

View file

@ -344,6 +344,75 @@ suite('markdown: find all references', () => {
);
});
test('Should support finding references to http uri', async () => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com)`,
`[no](https://example.com)`,
`[2](http://example.com)`,
`[3]: http://example.com`,
));
const refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([doc]));
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 2 },
{ uri, line: 3 },
);
});
test('Should consider paths when finding references to http uri', async () => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com/cat)`,
`[2](http://example.com)`,
`[3](http://example.com/dog)`,
`[4](http://example.com/cat/looong)`,
`[5](http://example.com/cat)`,
));
const refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([doc]));
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 4 },
);
});
test('Should support finding references to http uri across files', async () => {
const uri1 = workspacePath('doc.md');
const uri2 = workspacePath('doc2.md');
const doc = new InMemoryDocument(uri1, joinLines(
`[1](http://example.com)`,
`[3]: http://example.com`,
));
const refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([
doc,
new InMemoryDocument(uri2, joinLines(
`[other](http://example.com)`,
))
]));
assertReferencesEqual(refs!,
{ uri: uri1, line: 0 },
{ uri: uri1, line: 1 },
{ uri: uri2, line: 0 },
);
});
test('Should support finding references to autolinked http links', async () => {
const uri = workspacePath('doc.md');
const doc = new InMemoryDocument(uri, joinLines(
`[1](http://example.com)`,
`<http://example.com>`,
));
const refs = await getReferences(doc, new vscode.Position(0, 13), new InMemoryWorkspaceMarkdownDocuments([doc]));
assertReferencesEqual(refs!,
{ uri, line: 0 },
{ uri, line: 1 },
);
});
suite('Reference links', () => {
test('Should find reference links within file from link', async () => {
const docUri = workspacePath('doc.md');

View file

@ -356,4 +356,32 @@ suite('markdown: rename', () => {
assert.strictEqual(info!.placeholder, 'a B c');
assertRangeEqual(info!.range, new vscode.Range(1, 8, 1, 13));
});
test('Rename on http uri should work ', async () => {
const uri1 = workspacePath('doc.md');
const uri2 = workspacePath('doc2.md');
const doc = new InMemoryDocument(uri1, joinLines(
`[1](http://example.com)`,
`[2]: http://example.com`,
`<http://example.com>`,
));
const edit = await getRenameEdits(doc, new vscode.Position(1, 10), "https://example.com/sub", new InMemoryWorkspaceMarkdownDocuments([
doc,
new InMemoryDocument(uri2, joinLines(
`[4](http://example.com)`,
))
]));
assertEditsEqual(edit!, {
uri: uri1, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 22), 'https://example.com/sub'),
new vscode.TextEdit(new vscode.Range(1, 5, 1, 23), 'https://example.com/sub'),
new vscode.TextEdit(new vscode.Range(2, 1, 2, 19), 'https://example.com/sub'),
]
}, {
uri: uri2, edits: [
new vscode.TextEdit(new vscode.Range(0, 4, 0, 22), 'https://example.com/sub'),
]
});
});
});