Skip to content

Commit

Permalink
Navigate to anchor using xref (#744)
Browse files Browse the repository at this point in the history
* Navigate to anchor using xref

* Add tests
  • Loading branch information
ggrossetie authored Jul 11, 2023
1 parent 0c132e9 commit 116b676
Show file tree
Hide file tree
Showing 2 changed files with 116 additions and 10 deletions.
68 changes: 66 additions & 2 deletions src/features/documentLinkProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import { AsciidocParser } from '../asciidocParser'
* Reference: https://gist.github.com/dperini/729294
*/
// eslint-disable-next-line max-len
const urlRx = /(?:(?:https?|ftp|irc):)?\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4])|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+[a-z\u00a1-\uffff]{2,}\.?)(?::\d{2,5})?(?:[/?#]\S*)?/ig
const urlRx = /(?:(?:https?|ftp|irc):)?\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4])|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+[a-z\u00a1-\uffff]{2,}\.?)(?::\d{2,5})?(?:[/?#][^[]*)?/ig
const inlineAnchorRx = /^\[\[(?<id>[^,]+)(?:,[^\]]+)*]]$/m
const xrefRx = /xref:(?<target>[^#|^[]+)(?<fragment>#[^[]+)?\[[^\]]*]/ig
const localize = nls.loadMessageBundle()

function normalizeLink (
Expand Down Expand Up @@ -49,6 +51,9 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
const baseDocumentRegexIncludes = new Map()
const results: vscode.DocumentLink[] = []
const lines = textDocument.getText().split('\n')
const anchors = {}
const xrefProxies = []
const base = textDocument.uri.path.substring(0, textDocument.uri.path.lastIndexOf('/'))
for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) {
const line = lines[lineNumber]
const match = includeDirective.exec(line)
Expand All @@ -74,6 +79,66 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
}
}
}
if (line.startsWith('[[') && line.endsWith(']]')) {
const inlineAnchorFound = line.match(inlineAnchorRx)
if (inlineAnchorFound) {
const inlineAnchorId = inlineAnchorFound.groups.id
anchors[`${textDocument.uri.path}#${inlineAnchorId}`] = {
lineNumber: lineNumber + 1,
}
}
}
if (line.includes('xref:')) {
const xrefsFound = line.matchAll(xrefRx)
if (xrefsFound) {
for (const xrefFound of xrefsFound) {
const index = xrefFound.index
const target = xrefFound.groups.target
let fragment = xrefFound.groups.fragment || ''
const originalTarget = `${target}${fragment}`
let targetUri
if (path.isAbsolute(target)) {
targetUri = vscode.Uri.parse(target)
} else {
targetUri = vscode.Uri.parse(base + '/' + target)
}
if (targetUri.path === textDocument.uri.path) {
xrefProxies.push((anchors) => {
const anchorFound = anchors[`${targetUri.path}${fragment}`]
if (anchorFound) {
fragment = `#L${anchorFound.lineNumber}`
}
const documentLink = new vscode.DocumentLink(
new vscode.Range(
// exclude xref: prefix
new vscode.Position(lineNumber, index + 5),
new vscode.Position(lineNumber, originalTarget.length + index + 5)
),
normalizeLink(textDocument, `${target}${fragment}`, base)
)
documentLink.tooltip = localize('documentLink.openFile.tooltip', 'Open file {0}', target)
return documentLink
})
} else {
const documentLink = new vscode.DocumentLink(
new vscode.Range(
new vscode.Position(lineNumber, index + 5),
new vscode.Position(lineNumber, originalTarget.length + index + 5)
),
normalizeLink(textDocument, `${target}${fragment}`, base)
)
documentLink.tooltip = localize('documentLink.openFile.tooltip', 'Open file {0}', target)
results.push(documentLink)
}
}
}
}
}

if (xrefProxies && xrefProxies.length > 0) {
for (const xrefProxy of xrefProxies) {
results.push(xrefProxy(anchors))
}
}

// find a corrected mapping for line numbers
Expand All @@ -94,7 +159,6 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {

// create include links
if (baseDocumentProcessorIncludes) {
const base = path.dirname(textDocument.uri.fsPath)
baseDocumentProcessorIncludes.forEach((entry) => {
const lineNo = entry.position - 1
const documentLink = new vscode.DocumentLink(
Expand Down
58 changes: 50 additions & 8 deletions src/test/documentLinkProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as assert from 'assert'
import 'mocha'
import * as vscode from 'vscode'
import LinkProvider from '../features/documentLinkProvider'
import { InMemoryDocument } from './inMemoryDocument'

const testFileName = vscode.Uri.file('test.md')

const noopToken = new class implements vscode.CancellationToken {
private _onCancellationRequestedEmitter = new vscode.EventEmitter<void>()
public onCancellationRequested = this._onCancellationRequestedEmitter.event

get isCancellationRequested () { return false }
}()

async function getLinksForFile (fileContents: string) {
const doc = new InMemoryDocument(testFileName, fileContents)
async function getLinksForFile (fileContents: string, testFileName?: vscode.Uri) {
const doc = new InMemoryDocument(testFileName || vscode.Uri.file('test.adoc'), fileContents)
const provider = new LinkProvider()
return provider.provideDocumentLinks(doc, noopToken)
}
Expand Down Expand Up @@ -82,4 +76,52 @@ b
assertRangeEqual(link.range, new vscode.Range(4, 9, 4, 16))
}
})

test('Should detect inline anchor using [[idname]] syntax and xref', async () => {
const links = await getLinksForFile(`= Title
[[first-section]]
== Section Title
Paragraph.
== Second Section Title
See xref:test.adoc#first-section[]
`)
assert.strictEqual(links.length, 1)
const [link] = links

assert.strictEqual(link.target.scheme, 'command')
assert.deepStrictEqual(link.target.path, '_asciidoc.openDocumentLink')
assert.strictEqual(link.target.query, JSON.stringify({
path: 'test.adoc',
fragment: 'L3',
}))
assertRangeEqual(link.range, new vscode.Range(9, 9, 9, 32))
})

test('Should detect xref and inline anchor using [[idname]] syntax', async () => {
const links = await getLinksForFile(`= Title
[[first-section]]
== Section Title
Paragraph.
See xref:test.adoc#second-section[]
[[second-section]]
== Second Section Title
`)
assert.strictEqual(links.length, 1)
const [link] = links
assert.strictEqual(link.target.scheme, 'command')
assert.deepStrictEqual(link.target.path, '_asciidoc.openDocumentLink')
assert.strictEqual(link.target.query, JSON.stringify({
path: 'test.adoc',
fragment: 'L9',
}))
assertRangeEqual(link.range, new vscode.Range(6, 9, 6, 33))
})
})

0 comments on commit 116b676

Please sign in to comment.