Skip to content

Commit 830987e

Browse files
committed
Refactor document link opening
- Extract out of command - Try to preserve uri instead of converting to path - Better handle case with absolute file path when there is no workspace
1 parent d1f72b5 commit 830987e

File tree

3 files changed

+169
-156
lines changed

3 files changed

+169
-156
lines changed

extensions/markdown-language-features/src/commands/openDocumentLink.ts

Lines changed: 3 additions & 136 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
import * as vscode from 'vscode';
77
import { Command } from '../commandManager';
88
import { MarkdownEngine } from '../markdownEngine';
9-
import { TableOfContentsProvider } from '../tableOfContentsProvider';
10-
import { isMarkdownFile } from '../util/file';
11-
import { extname } from '../util/path';
12-
9+
import { openDocumentLink } from '../util/openDocumentLink';
1310

1411
type UriComponents = {
1512
readonly scheme?: string;
@@ -25,11 +22,6 @@ export interface OpenDocumentLinkArgs {
2522
readonly fromResource: UriComponents;
2623
}
2724

28-
enum OpenMarkdownLinks {
29-
beside = 'beside',
30-
currentGroup = 'currentGroup',
31-
}
32-
3325
export class OpenDocumentLinkCommand implements Command {
3426
private static readonly id = '_markdown.openDocumentLink';
3527
public readonly id = OpenDocumentLinkCommand.id;
@@ -60,101 +52,9 @@ export class OpenDocumentLinkCommand implements Command {
6052
) { }
6153

6254
public async execute(args: OpenDocumentLinkArgs) {
63-
return OpenDocumentLinkCommand.execute(this.engine, args);
64-
}
65-
66-
public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs): Promise<void> {
6755
const fromResource = vscode.Uri.parse('').with(args.fromResource);
68-
const targetResource = reviveUri(args.parts);
69-
const column = this.getViewColumn(fromResource);
70-
71-
if (await OpenDocumentLinkCommand.tryNavigateToFragmentInActiveEditor(engine, targetResource, args)) {
72-
return;
73-
}
74-
75-
let targetResourceStat: vscode.FileStat | undefined;
76-
try {
77-
targetResourceStat = await vscode.workspace.fs.stat(targetResource);
78-
} catch {
79-
// noop
80-
}
81-
82-
if (typeof targetResourceStat === 'undefined') {
83-
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
84-
if (extname(targetResource.path) === '') {
85-
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
86-
try {
87-
const stat = await vscode.workspace.fs.stat(dotMdResource);
88-
if (stat.type === vscode.FileType.File) {
89-
await OpenDocumentLinkCommand.tryOpenMdFile(engine, dotMdResource, column, args);
90-
return;
91-
}
92-
} catch {
93-
// noop
94-
}
95-
}
96-
} else if (targetResourceStat.type === vscode.FileType.Directory) {
97-
return vscode.commands.executeCommand('revealInExplorer', targetResource);
98-
}
99-
100-
await OpenDocumentLinkCommand.tryOpenMdFile(engine, targetResource, column, args);
101-
}
102-
103-
private static async tryOpenMdFile(engine: MarkdownEngine, resource: vscode.Uri, column: vscode.ViewColumn, args: OpenDocumentLinkArgs): Promise<boolean> {
104-
await vscode.commands.executeCommand('vscode.open', resource, column);
105-
return OpenDocumentLinkCommand.tryNavigateToFragmentInActiveEditor(engine, resource, args);
106-
}
107-
108-
private static async tryNavigateToFragmentInActiveEditor(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs): Promise<boolean> {
109-
const activeEditor = vscode.window.activeTextEditor;
110-
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
111-
if (isMarkdownFile(activeEditor.document)) {
112-
if (await this.tryRevealLineUsingTocFragment(engine, activeEditor, args.fragment)) {
113-
return true;
114-
}
115-
}
116-
this.tryRevealLineUsingLineFragment(activeEditor, args.fragment);
117-
return true;
118-
}
119-
return false;
120-
}
121-
122-
private static getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
123-
const config = vscode.workspace.getConfiguration('markdown', resource);
124-
const openLinks = config.get<OpenMarkdownLinks>('links.openLocation', OpenMarkdownLinks.currentGroup);
125-
switch (openLinks) {
126-
case OpenMarkdownLinks.beside:
127-
return vscode.ViewColumn.Beside;
128-
case OpenMarkdownLinks.currentGroup:
129-
default:
130-
return vscode.ViewColumn.Active;
131-
}
132-
}
133-
134-
private static async tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
135-
const toc = new TableOfContentsProvider(engine, editor.document);
136-
const entry = await toc.lookup(fragment);
137-
if (entry) {
138-
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
139-
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
140-
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
141-
return true;
142-
}
143-
return false;
144-
}
145-
146-
private static tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean {
147-
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
148-
if (lineNumberFragment) {
149-
const line = +lineNumberFragment[1] - 1;
150-
if (!isNaN(line)) {
151-
const lineStart = new vscode.Range(line, 0, line, 0);
152-
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
153-
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
154-
return true;
155-
}
156-
}
157-
return false;
56+
const targetResource = reviveUri(args.parts).with({ fragment: args.fragment });
57+
return openDocumentLink(this.engine, targetResource, fromResource);
15858
}
15959
}
16060

@@ -164,36 +64,3 @@ function reviveUri(parts: any) {
16464
}
16565
return vscode.Uri.parse('').with(parts);
16666
}
167-
168-
export async function resolveLinkToMarkdownFile(path: string): Promise<vscode.Uri | undefined> {
169-
try {
170-
const standardLink = await tryResolveLinkToMarkdownFile(path);
171-
if (standardLink) {
172-
return standardLink;
173-
}
174-
} catch {
175-
// Noop
176-
}
177-
178-
// If no extension, try with `.md` extension
179-
if (extname(path) === '') {
180-
return tryResolveLinkToMarkdownFile(path + '.md');
181-
}
182-
183-
return undefined;
184-
}
185-
186-
async function tryResolveLinkToMarkdownFile(path: string): Promise<vscode.Uri | undefined> {
187-
const resource = vscode.Uri.file(path);
188-
189-
let document: vscode.TextDocument;
190-
try {
191-
document = await vscode.workspace.openTextDocument(resource);
192-
} catch {
193-
return undefined;
194-
}
195-
if (isMarkdownFile(document)) {
196-
return document.uri;
197-
}
198-
return undefined;
199-
}

extensions/markdown-language-features/src/features/preview.ts

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,12 @@
55

66
import * as vscode from 'vscode';
77
import * as nls from 'vscode-nls';
8-
import { OpenDocumentLinkCommand, resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
98
import { Logger } from '../logger';
109
import { MarkdownEngine } from '../markdownEngine';
1110
import { MarkdownContributionProvider } from '../markdownExtensions';
1211
import { Disposable } from '../util/dispose';
1312
import { isMarkdownFile } from '../util/file';
13+
import { openDocumentLink, resolveDocumentLink, resolveLinkToMarkdownFile } from '../util/openDocumentLink';
1414
import * as path from '../util/path';
1515
import { WebviewResourceProvider } from '../util/resources';
1616
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor';
@@ -429,34 +429,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
429429

430430

431431
private async onDidClickPreviewLink(href: string) {
432-
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
433-
let hrefParts: vscode.Uri | undefined;
434-
435-
if (hrefPath[0] === '/') {
436-
// Absolute path. Try to resolve relative to the workspace
437-
const workspace = vscode.workspace.getWorkspaceFolder(this.resource);
438-
if (workspace) {
439-
hrefParts = vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1));
440-
hrefPath = hrefParts.path;
441-
}
442-
} else {
443-
// Relative path. Resolve relative to the md file
444-
const dirnameUri = this.resource.with({ path: path.dirname(this.resource.path) });
445-
hrefParts = vscode.Uri.joinPath(dirnameUri, hrefPath);
446-
hrefPath = hrefParts.path;
447-
}
432+
const targetResource = resolveDocumentLink(href, this.resource);
448433

449434
const config = vscode.workspace.getConfiguration('markdown', this.resource);
450435
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
451436
if (openLinks === 'inPreview') {
452-
const markdownLink = await resolveLinkToMarkdownFile(hrefPath);
437+
const markdownLink = await resolveLinkToMarkdownFile(targetResource);
453438
if (markdownLink) {
454-
this.delegate.openPreviewLinkToMarkdownFile(markdownLink, fragment);
439+
this.delegate.openPreviewLinkToMarkdownFile(markdownLink, targetResource.fragment);
455440
return;
456441
}
457442
}
458443

459-
OpenDocumentLinkCommand.execute(this.engine, { parts: hrefParts ?? { path: hrefPath }, fragment, fromResource: this.resource.toJSON() });
444+
return openDocumentLink(this.engine, targetResource, this.resource);
460445
}
461446

462447
//#region WebviewResourceProvider
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as path from 'path';
7+
import * as vscode from 'vscode';
8+
import { MarkdownEngine } from '../markdownEngine';
9+
import { TableOfContentsProvider } from '../tableOfContentsProvider';
10+
import { isMarkdownFile } from '../util/file';
11+
import { extname } from '../util/path';
12+
13+
export interface OpenDocumentLinkArgs {
14+
readonly parts: vscode.Uri;
15+
readonly fragment: string;
16+
readonly fromResource: vscode.Uri;
17+
}
18+
19+
enum OpenMarkdownLinks {
20+
beside = 'beside',
21+
currentGroup = 'currentGroup',
22+
}
23+
24+
export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vscode.Uri {
25+
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
26+
27+
if (hrefPath[0] === '/') {
28+
// Absolute path. Try to resolve relative to the workspace
29+
const workspace = vscode.workspace.getWorkspaceFolder(markdownFile);
30+
if (workspace) {
31+
return vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1)).with({ fragment });
32+
}
33+
}
34+
35+
// Relative path. Resolve relative to the md file
36+
const dirnameUri = markdownFile.with({ path: path.dirname(markdownFile.path) });
37+
return vscode.Uri.joinPath(dirnameUri, hrefPath).with({ fragment });
38+
}
39+
40+
export async function openDocumentLink(engine: MarkdownEngine, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise<void> {
41+
const column = getViewColumn(fromResource);
42+
43+
if (await tryNavigateToFragmentInActiveEditor(engine, targetResource)) {
44+
return;
45+
}
46+
47+
let targetResourceStat: vscode.FileStat | undefined;
48+
try {
49+
targetResourceStat = await vscode.workspace.fs.stat(targetResource);
50+
} catch {
51+
// noop
52+
}
53+
54+
if (typeof targetResourceStat === 'undefined') {
55+
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
56+
if (extname(targetResource.path) === '') {
57+
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
58+
try {
59+
const stat = await vscode.workspace.fs.stat(dotMdResource);
60+
if (stat.type === vscode.FileType.File) {
61+
await tryOpenMdFile(engine, dotMdResource, column);
62+
return;
63+
}
64+
} catch {
65+
// noop
66+
}
67+
}
68+
} else if (targetResourceStat.type === vscode.FileType.Directory) {
69+
return vscode.commands.executeCommand('revealInExplorer', targetResource);
70+
}
71+
72+
await tryOpenMdFile(engine, targetResource, column);
73+
}
74+
75+
async function tryOpenMdFile(engine: MarkdownEngine, resource: vscode.Uri, column: vscode.ViewColumn): Promise<boolean> {
76+
await vscode.commands.executeCommand('vscode.open', resource, column);
77+
return tryNavigateToFragmentInActiveEditor(engine, resource);
78+
}
79+
80+
async function tryNavigateToFragmentInActiveEditor(engine: MarkdownEngine, resource: vscode.Uri): Promise<boolean> {
81+
const activeEditor = vscode.window.activeTextEditor;
82+
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
83+
if (isMarkdownFile(activeEditor.document)) {
84+
if (await tryRevealLineUsingTocFragment(engine, activeEditor, resource.fragment)) {
85+
return true;
86+
}
87+
}
88+
tryRevealLineUsingLineFragment(activeEditor, resource.fragment);
89+
return true;
90+
}
91+
return false;
92+
}
93+
94+
function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
95+
const config = vscode.workspace.getConfiguration('markdown', resource);
96+
const openLinks = config.get<OpenMarkdownLinks>('links.openLocation', OpenMarkdownLinks.currentGroup);
97+
switch (openLinks) {
98+
case OpenMarkdownLinks.beside:
99+
return vscode.ViewColumn.Beside;
100+
case OpenMarkdownLinks.currentGroup:
101+
default:
102+
return vscode.ViewColumn.Active;
103+
}
104+
}
105+
106+
async function tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
107+
const toc = new TableOfContentsProvider(engine, editor.document);
108+
const entry = await toc.lookup(fragment);
109+
if (entry) {
110+
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
111+
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
112+
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
113+
return true;
114+
}
115+
return false;
116+
}
117+
118+
function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean {
119+
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
120+
if (lineNumberFragment) {
121+
const line = +lineNumberFragment[1] - 1;
122+
if (!isNaN(line)) {
123+
const lineStart = new vscode.Range(line, 0, line, 0);
124+
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
125+
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
126+
return true;
127+
}
128+
}
129+
return false;
130+
}
131+
132+
export async function resolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
133+
try {
134+
const standardLink = await tryResolveLinkToMarkdownFile(resource);
135+
if (standardLink) {
136+
return standardLink;
137+
}
138+
} catch {
139+
// Noop
140+
}
141+
142+
// If no extension, try with `.md` extension
143+
if (extname(resource.path) === '') {
144+
return tryResolveLinkToMarkdownFile(resource.with({ path: resource.path + '.md' }));
145+
}
146+
147+
return undefined;
148+
}
149+
150+
async function tryResolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
151+
let document: vscode.TextDocument;
152+
try {
153+
document = await vscode.workspace.openTextDocument(resource);
154+
} catch {
155+
return undefined;
156+
}
157+
if (isMarkdownFile(document)) {
158+
return document.uri;
159+
}
160+
return undefined;
161+
}

0 commit comments

Comments
 (0)