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
This commit is contained in:
Matt Bierner
2021-10-13 19:27:35 -07:00
parent d1f72b5420
commit 830987eac3
3 changed files with 169 additions and 156 deletions

View File

@@ -6,10 +6,7 @@
import * as vscode from 'vscode';
import { Command } from '../commandManager';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { isMarkdownFile } from '../util/file';
import { extname } from '../util/path';
import { openDocumentLink } from '../util/openDocumentLink';
type UriComponents = {
readonly scheme?: string;
@@ -25,11 +22,6 @@ export interface OpenDocumentLinkArgs {
readonly fromResource: UriComponents;
}
enum OpenMarkdownLinks {
beside = 'beside',
currentGroup = 'currentGroup',
}
export class OpenDocumentLinkCommand implements Command {
private static readonly id = '_markdown.openDocumentLink';
public readonly id = OpenDocumentLinkCommand.id;
@@ -60,101 +52,9 @@ export class OpenDocumentLinkCommand implements Command {
) { }
public async execute(args: OpenDocumentLinkArgs) {
return OpenDocumentLinkCommand.execute(this.engine, args);
}
public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs): Promise<void> {
const fromResource = vscode.Uri.parse('').with(args.fromResource);
const targetResource = reviveUri(args.parts);
const column = this.getViewColumn(fromResource);
if (await OpenDocumentLinkCommand.tryNavigateToFragmentInActiveEditor(engine, targetResource, args)) {
return;
}
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(targetResource);
} catch {
// noop
}
if (typeof targetResourceStat === 'undefined') {
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
if (extname(targetResource.path) === '') {
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
try {
const stat = await vscode.workspace.fs.stat(dotMdResource);
if (stat.type === vscode.FileType.File) {
await OpenDocumentLinkCommand.tryOpenMdFile(engine, dotMdResource, column, args);
return;
}
} catch {
// noop
}
}
} else if (targetResourceStat.type === vscode.FileType.Directory) {
return vscode.commands.executeCommand('revealInExplorer', targetResource);
}
await OpenDocumentLinkCommand.tryOpenMdFile(engine, targetResource, column, args);
}
private static async tryOpenMdFile(engine: MarkdownEngine, resource: vscode.Uri, column: vscode.ViewColumn, args: OpenDocumentLinkArgs): Promise<boolean> {
await vscode.commands.executeCommand('vscode.open', resource, column);
return OpenDocumentLinkCommand.tryNavigateToFragmentInActiveEditor(engine, resource, args);
}
private static async tryNavigateToFragmentInActiveEditor(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs): Promise<boolean> {
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
if (isMarkdownFile(activeEditor.document)) {
if (await this.tryRevealLineUsingTocFragment(engine, activeEditor, args.fragment)) {
return true;
}
}
this.tryRevealLineUsingLineFragment(activeEditor, args.fragment);
return true;
}
return false;
}
private static getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
const config = vscode.workspace.getConfiguration('markdown', resource);
const openLinks = config.get<OpenMarkdownLinks>('links.openLocation', OpenMarkdownLinks.currentGroup);
switch (openLinks) {
case OpenMarkdownLinks.beside:
return vscode.ViewColumn.Beside;
case OpenMarkdownLinks.currentGroup:
default:
return vscode.ViewColumn.Active;
}
}
private static async tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
const toc = new TableOfContentsProvider(engine, editor.document);
const entry = await toc.lookup(fragment);
if (entry) {
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
return false;
}
private static tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean {
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
if (lineNumberFragment) {
const line = +lineNumberFragment[1] - 1;
if (!isNaN(line)) {
const lineStart = new vscode.Range(line, 0, line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
}
return false;
const targetResource = reviveUri(args.parts).with({ fragment: args.fragment });
return openDocumentLink(this.engine, targetResource, fromResource);
}
}
@@ -164,36 +64,3 @@ function reviveUri(parts: any) {
}
return vscode.Uri.parse('').with(parts);
}
export async function resolveLinkToMarkdownFile(path: string): Promise<vscode.Uri | undefined> {
try {
const standardLink = await tryResolveLinkToMarkdownFile(path);
if (standardLink) {
return standardLink;
}
} catch {
// Noop
}
// If no extension, try with `.md` extension
if (extname(path) === '') {
return tryResolveLinkToMarkdownFile(path + '.md');
}
return undefined;
}
async function tryResolveLinkToMarkdownFile(path: string): Promise<vscode.Uri | undefined> {
const resource = vscode.Uri.file(path);
let document: vscode.TextDocument;
try {
document = await vscode.workspace.openTextDocument(resource);
} catch {
return undefined;
}
if (isMarkdownFile(document)) {
return document.uri;
}
return undefined;
}

View File

@@ -5,12 +5,12 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { OpenDocumentLinkCommand, resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
import { Logger } from '../logger';
import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { openDocumentLink, resolveDocumentLink, resolveLinkToMarkdownFile } from '../util/openDocumentLink';
import * as path from '../util/path';
import { WebviewResourceProvider } from '../util/resources';
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor';
@@ -429,34 +429,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private async onDidClickPreviewLink(href: string) {
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
let hrefParts: vscode.Uri | undefined;
if (hrefPath[0] === '/') {
// Absolute path. Try to resolve relative to the workspace
const workspace = vscode.workspace.getWorkspaceFolder(this.resource);
if (workspace) {
hrefParts = vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1));
hrefPath = hrefParts.path;
}
} else {
// Relative path. Resolve relative to the md file
const dirnameUri = this.resource.with({ path: path.dirname(this.resource.path) });
hrefParts = vscode.Uri.joinPath(dirnameUri, hrefPath);
hrefPath = hrefParts.path;
}
const targetResource = resolveDocumentLink(href, this.resource);
const config = vscode.workspace.getConfiguration('markdown', this.resource);
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
if (openLinks === 'inPreview') {
const markdownLink = await resolveLinkToMarkdownFile(hrefPath);
const markdownLink = await resolveLinkToMarkdownFile(targetResource);
if (markdownLink) {
this.delegate.openPreviewLinkToMarkdownFile(markdownLink, fragment);
this.delegate.openPreviewLinkToMarkdownFile(markdownLink, targetResource.fragment);
return;
}
}
OpenDocumentLinkCommand.execute(this.engine, { parts: hrefParts ?? { path: hrefPath }, fragment, fromResource: this.resource.toJSON() });
return openDocumentLink(this.engine, targetResource, this.resource);
}
//#region WebviewResourceProvider

View File

@@ -0,0 +1,161 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { isMarkdownFile } from '../util/file';
import { extname } from '../util/path';
export interface OpenDocumentLinkArgs {
readonly parts: vscode.Uri;
readonly fragment: string;
readonly fromResource: vscode.Uri;
}
enum OpenMarkdownLinks {
beside = 'beside',
currentGroup = 'currentGroup',
}
export function resolveDocumentLink(href: string, markdownFile: vscode.Uri): vscode.Uri {
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
if (hrefPath[0] === '/') {
// Absolute path. Try to resolve relative to the workspace
const workspace = vscode.workspace.getWorkspaceFolder(markdownFile);
if (workspace) {
return vscode.Uri.joinPath(workspace.uri, hrefPath.slice(1)).with({ fragment });
}
}
// Relative path. Resolve relative to the md file
const dirnameUri = markdownFile.with({ path: path.dirname(markdownFile.path) });
return vscode.Uri.joinPath(dirnameUri, hrefPath).with({ fragment });
}
export async function openDocumentLink(engine: MarkdownEngine, targetResource: vscode.Uri, fromResource: vscode.Uri): Promise<void> {
const column = getViewColumn(fromResource);
if (await tryNavigateToFragmentInActiveEditor(engine, targetResource)) {
return;
}
let targetResourceStat: vscode.FileStat | undefined;
try {
targetResourceStat = await vscode.workspace.fs.stat(targetResource);
} catch {
// noop
}
if (typeof targetResourceStat === 'undefined') {
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
if (extname(targetResource.path) === '') {
const dotMdResource = targetResource.with({ path: targetResource.path + '.md' });
try {
const stat = await vscode.workspace.fs.stat(dotMdResource);
if (stat.type === vscode.FileType.File) {
await tryOpenMdFile(engine, dotMdResource, column);
return;
}
} catch {
// noop
}
}
} else if (targetResourceStat.type === vscode.FileType.Directory) {
return vscode.commands.executeCommand('revealInExplorer', targetResource);
}
await tryOpenMdFile(engine, targetResource, column);
}
async function tryOpenMdFile(engine: MarkdownEngine, resource: vscode.Uri, column: vscode.ViewColumn): Promise<boolean> {
await vscode.commands.executeCommand('vscode.open', resource, column);
return tryNavigateToFragmentInActiveEditor(engine, resource);
}
async function tryNavigateToFragmentInActiveEditor(engine: MarkdownEngine, resource: vscode.Uri): Promise<boolean> {
const activeEditor = vscode.window.activeTextEditor;
if (activeEditor?.document.uri.fsPath === resource.fsPath) {
if (isMarkdownFile(activeEditor.document)) {
if (await tryRevealLineUsingTocFragment(engine, activeEditor, resource.fragment)) {
return true;
}
}
tryRevealLineUsingLineFragment(activeEditor, resource.fragment);
return true;
}
return false;
}
function getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
const config = vscode.workspace.getConfiguration('markdown', resource);
const openLinks = config.get<OpenMarkdownLinks>('links.openLocation', OpenMarkdownLinks.currentGroup);
switch (openLinks) {
case OpenMarkdownLinks.beside:
return vscode.ViewColumn.Beside;
case OpenMarkdownLinks.currentGroup:
default:
return vscode.ViewColumn.Active;
}
}
async function tryRevealLineUsingTocFragment(engine: MarkdownEngine, editor: vscode.TextEditor, fragment: string): Promise<boolean> {
const toc = new TableOfContentsProvider(engine, editor.document);
const entry = await toc.lookup(fragment);
if (entry) {
const lineStart = new vscode.Range(entry.line, 0, entry.line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
return false;
}
function tryRevealLineUsingLineFragment(editor: vscode.TextEditor, fragment: string): boolean {
const lineNumberFragment = fragment.match(/^L(\d+)$/i);
if (lineNumberFragment) {
const line = +lineNumberFragment[1] - 1;
if (!isNaN(line)) {
const lineStart = new vscode.Range(line, 0, line, 0);
editor.selection = new vscode.Selection(lineStart.start, lineStart.end);
editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
return true;
}
}
return false;
}
export async function resolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
try {
const standardLink = await tryResolveLinkToMarkdownFile(resource);
if (standardLink) {
return standardLink;
}
} catch {
// Noop
}
// If no extension, try with `.md` extension
if (extname(resource.path) === '') {
return tryResolveLinkToMarkdownFile(resource.with({ path: resource.path + '.md' }));
}
return undefined;
}
async function tryResolveLinkToMarkdownFile(resource: vscode.Uri): Promise<vscode.Uri | undefined> {
let document: vscode.TextDocument;
try {
document = await vscode.workspace.openTextDocument(resource);
} catch {
return undefined;
}
if (isMarkdownFile(document)) {
return document.uri;
}
return undefined;
}