mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge vscode source through 1.62 release (#19981)
* Build breaks 1 * Build breaks * Build breaks * Build breaks * More build breaks * Build breaks (#2512) * Runtime breaks * Build breaks * Fix dialog location break * Update typescript * Fix ASAR break issue * Unit test breaks * Update distro * Fix breaks in ADO builds (#2513) * Bump to node 16 * Fix hygiene errors * Bump distro * Remove reference to node type * Delete vscode specific extension * Bump to node 16 in CI yaml * Skip integration tests in CI builds (while fixing) * yarn.lock update * Bump moment dependency in remote yarn * Fix drop-down chevron style * Bump to node 16 * Remove playwrite from ci.yaml * Skip building build scripts in hygine check
This commit is contained in:
@@ -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,95 +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);
|
||||
|
||||
const didOpen = await this.tryOpen(engine, targetResource, args, column);
|
||||
if (didOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (extname(targetResource.path) === '') {
|
||||
await this.tryOpen(engine, targetResource.with({ path: targetResource.path + '.md' }), args, column);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static async tryOpen(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn): Promise<boolean> {
|
||||
const tryUpdateForActiveFile = async (): Promise<boolean> => {
|
||||
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
|
||||
if (vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
|
||||
await this.tryRevealLine(engine, vscode.window.activeTextEditor, args.fragment);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (await tryUpdateForActiveFile()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let stat: vscode.FileStat;
|
||||
try {
|
||||
stat = await vscode.workspace.fs.stat(resource);
|
||||
if (stat.type === vscode.FileType.Directory) {
|
||||
await vscode.commands.executeCommand('revealInExplorer', resource);
|
||||
return true;
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
// If resource doesn't exist, execute `vscode.open` either way so an error
|
||||
// notification is shown to the user with a create file action #113475
|
||||
}
|
||||
|
||||
try {
|
||||
await vscode.commands.executeCommand('vscode.open', resource, column);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tryUpdateForActiveFile();
|
||||
}
|
||||
|
||||
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 tryRevealLine(engine: MarkdownEngine, editor: vscode.TextEditor, fragment?: string) {
|
||||
if (fragment) {
|
||||
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);
|
||||
return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
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);
|
||||
return editor.revealRange(lineStart, vscode.TextEditorRevealType.AtTop);
|
||||
}
|
||||
}
|
||||
}
|
||||
const targetResource = reviveUri(args.parts).with({ fragment: args.fragment });
|
||||
return openDocumentLink(this.engine, targetResource, fromResource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,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;
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ async function showPreview(
|
||||
const resourceColumn = (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One;
|
||||
webviewManager.openDynamicPreview(resource, {
|
||||
resourceColumn: resourceColumn,
|
||||
previewColumn: previewSettings.sideBySide ? resourceColumn + 1 : resourceColumn,
|
||||
previewColumn: previewSettings.sideBySide ? vscode.ViewColumn.Beside : resourceColumn,
|
||||
locked: !!previewSettings.locked
|
||||
});
|
||||
|
||||
|
||||
@@ -83,3 +83,4 @@ function registerMarkdownCommands(
|
||||
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
|
||||
return commandManager;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ function parseLink(
|
||||
document: vscode.TextDocument,
|
||||
link: string,
|
||||
): { uri: vscode.Uri, tooltip?: string } | undefined {
|
||||
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link);
|
||||
|
||||
const cleanLink = stripAngleBrackets(link);
|
||||
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(cleanLink);
|
||||
if (externalSchemeUri) {
|
||||
// Normalize VS Code links to target currently running version
|
||||
if (isOfScheme(Schemes.vscode, link) || isOfScheme(Schemes['vscode-insiders'], link)) {
|
||||
@@ -89,6 +91,15 @@ function extractDocumentLink(
|
||||
}
|
||||
}
|
||||
|
||||
/* Used to strip brackets from the markdown link
|
||||
<http://example.com> will be transformed to
|
||||
http://example.com
|
||||
*/
|
||||
export function stripAngleBrackets(link: string) {
|
||||
const bracketMatcher = /^<(.*)>$/;
|
||||
return link.replace(bracketMatcher, '$1');
|
||||
}
|
||||
|
||||
export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g;
|
||||
private readonly referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g;
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Token } from 'markdown-it';
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
|
||||
const rangeLimit = 5000;
|
||||
|
||||
interface MarkdownItTokenWithMap extends Token {
|
||||
map: [number, number];
|
||||
}
|
||||
|
||||
export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider {
|
||||
|
||||
constructor(
|
||||
@@ -84,10 +88,14 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
const isStartRegion = (t: string) => /^\s*<!--\s*#?region\b.*-->/.test(t);
|
||||
const isEndRegion = (t: string) => /^\s*<!--\s*#?endregion\b.*-->/.test(t);
|
||||
|
||||
const isRegionMarker = (token: Token) =>
|
||||
token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content));
|
||||
const isRegionMarker = (token: Token): token is MarkdownItTokenWithMap =>
|
||||
!!token.map && token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content));
|
||||
|
||||
const isFoldableToken = (token: Token): token is MarkdownItTokenWithMap => {
|
||||
if (!token.map) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isFoldableToken = (token: Token): boolean => {
|
||||
switch (token.type) {
|
||||
case 'fence':
|
||||
case 'list_item_open':
|
||||
|
||||
@@ -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';
|
||||
@@ -355,7 +355,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
vscode.workspace.openTextDocument(this._resource)
|
||||
await vscode.workspace.openTextDocument(this._resource)
|
||||
.then(vscode.window.showTextDocument)
|
||||
.then(undefined, () => {
|
||||
vscode.window.showErrorMessage(localize('preview.clickOpenFailed', 'Could not open {0}', this._resource.toString()));
|
||||
@@ -406,6 +406,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
private getWebviewOptions(): vscode.WebviewOptions {
|
||||
return {
|
||||
enableScripts: true,
|
||||
enableForms: false,
|
||||
localResourceRoots: this.getLocalResourceRoots()
|
||||
};
|
||||
}
|
||||
@@ -428,29 +429,19 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
|
||||
|
||||
private async onDidClickPreviewLink(href: string) {
|
||||
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
|
||||
|
||||
if (hrefPath[0] !== '/') {
|
||||
// We perviously already resolve absolute paths.
|
||||
// Now make sure we handle relative file paths
|
||||
const dirnameUri = vscode.Uri.parse(path.dirname(this.resource.path));
|
||||
hrefPath = vscode.Uri.joinPath(dirnameUri, hrefPath).path;
|
||||
} else {
|
||||
// Handle any normalized file paths
|
||||
hrefPath = vscode.Uri.parse(hrefPath.replace('/file', '')).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: { path: hrefPath }, fragment, fromResource: this.resource.toJSON() });
|
||||
return openDocumentLink(this.engine, targetResource, this.resource);
|
||||
}
|
||||
|
||||
//#region WebviewResourceProvider
|
||||
@@ -531,7 +522,7 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
|
||||
}));
|
||||
|
||||
this._register(this.preview.onScroll((scrollInfo) => {
|
||||
topmostLineMonitor.setPreviousEditorLine(scrollInfo);
|
||||
topmostLineMonitor.setPreviousStaticEditorLine(scrollInfo);
|
||||
}));
|
||||
|
||||
this._register(topmostLineMonitor.onDidChanged(event => {
|
||||
|
||||
@@ -81,7 +81,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
|
||||
// When at a markdown file, apply existing scroll settings
|
||||
if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) {
|
||||
const line = this._topmostLineMonitor.getPreviousEditorLineByUri(textEditor.document.uri);
|
||||
const line = this._topmostLineMonitor.getPreviousStaticEditorLineByUri(textEditor.document.uri);
|
||||
if (line) {
|
||||
scrollEditorToLine(line, textEditor);
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
|
||||
document: vscode.TextDocument,
|
||||
webview: vscode.WebviewPanel
|
||||
): Promise<void> {
|
||||
const lineNumber = this._topmostLineMonitor.getPreviousEditorLineByUri(document.uri);
|
||||
const lineNumber = this._topmostLineMonitor.getPreviousTextEditorLineByUri(document.uri);
|
||||
const preview = StaticMarkdownPreview.revive(
|
||||
document.uri,
|
||||
webview,
|
||||
|
||||
@@ -2,11 +2,15 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Token } from 'markdown-it';
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
|
||||
|
||||
interface MarkdownItTokenWithMap extends Token {
|
||||
map: [number, number];
|
||||
}
|
||||
|
||||
export default class MarkdownSmartSelect implements vscode.SelectionRangeProvider {
|
||||
|
||||
constructor(
|
||||
@@ -96,8 +100,8 @@ function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean,
|
||||
}
|
||||
}
|
||||
|
||||
function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): Token[] {
|
||||
const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token));
|
||||
function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): MarkdownItTokenWithMap[] {
|
||||
const enclosingTokens = tokens.filter((token): token is MarkdownItTokenWithMap => !!token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token));
|
||||
if (enclosingTokens.length === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -105,7 +109,7 @@ function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, p
|
||||
return sortedTokens;
|
||||
}
|
||||
|
||||
function createBlockRange(block: Token, document: vscode.TextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
function createBlockRange(block: MarkdownItTokenWithMap, document: vscode.TextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
if (block.type === 'fence') {
|
||||
return createFencedRange(block, cursorLine, document, parent);
|
||||
} else {
|
||||
@@ -144,7 +148,7 @@ function createInlineRange(document: vscode.TextDocument, cursorPosition: vscode
|
||||
return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection;
|
||||
}
|
||||
|
||||
function createFencedRange(token: Token, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange {
|
||||
function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, document: vscode.TextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange {
|
||||
const startLine = token.map[0];
|
||||
const endLine = token.map[1] - 1;
|
||||
const onFenceLine = cursorLine === startLine || cursorLine === endLine;
|
||||
|
||||
@@ -41,7 +41,7 @@ class VSCodeWorkspaceMarkdownDocumentProvider extends Disposable implements Work
|
||||
|
||||
for (let i = 0; i < resources.length; i += maxConcurrent) {
|
||||
const resourceBatch = resources.slice(i, i + maxConcurrent);
|
||||
const documentBatch = (await Promise.all(resourceBatch.map(this.getMarkdownDocument))).filter((doc) => !!doc) as SkinnyTextDocument[];
|
||||
const documentBatch = (await Promise.all(resourceBatch.map(x => this.getMarkdownDocument(x)))).filter((doc) => !!doc) as SkinnyTextDocument[];
|
||||
docList.push(...documentBatch);
|
||||
}
|
||||
return docList;
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { MarkdownIt, Token } from 'markdown-it';
|
||||
import MarkdownIt = require('markdown-it');
|
||||
import Token = require('markdown-it/lib/token');
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions';
|
||||
import { Slugifier } from './slugify';
|
||||
@@ -14,11 +15,34 @@ import { WebviewResourceProvider } from './util/resources';
|
||||
|
||||
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
|
||||
|
||||
interface MarkdownItConfig {
|
||||
readonly breaks: boolean;
|
||||
readonly linkify: boolean;
|
||||
readonly typographer: boolean;
|
||||
}
|
||||
/**
|
||||
* Adds begin line index to the output via the 'data-line' data attribute.
|
||||
*/
|
||||
const pluginSourceMap: MarkdownIt.PluginSimple = (md): void => {
|
||||
// Set the attribute on every possible token.
|
||||
md.core.ruler.push('source_map_data_attribute', (state): void => {
|
||||
for (const token of state.tokens) {
|
||||
if (token.map && token.type !== 'inline') {
|
||||
token.attrSet('data-line', String(token.map[0]));
|
||||
token.attrJoin('class', 'code-line');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// The 'html_block' renderer doesn't respect `attrs`. We need to insert a marker.
|
||||
const originalHtmlBlockRenderer = md.renderer.rules['html_block'];
|
||||
if (originalHtmlBlockRenderer) {
|
||||
md.renderer.rules['html_block'] = (tokens, idx, options, env, self) => (
|
||||
`<div ${self.renderAttrs(tokens[idx])} ></div>\n` +
|
||||
originalHtmlBlockRenderer(tokens, idx, options, env, self)
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The markdown-it options that we expose in the settings.
|
||||
*/
|
||||
type MarkdownItConfig = Readonly<Required<Pick<MarkdownIt.Options, 'breaks' | 'linkify' | 'typographer'>>>;
|
||||
|
||||
class TokenCache {
|
||||
private cachedDocument?: {
|
||||
@@ -85,14 +109,15 @@ export class MarkdownEngine {
|
||||
|
||||
private async getEngine(config: MarkdownItConfig): Promise<MarkdownIt> {
|
||||
if (!this.md) {
|
||||
this.md = import('markdown-it').then(async markdownIt => {
|
||||
this.md = (async () => {
|
||||
const markdownIt = await import('markdown-it');
|
||||
let md: MarkdownIt = markdownIt(await getMarkdownOptions(() => md));
|
||||
|
||||
for (const plugin of this.contributionProvider.contributions.markdownItPlugins.values()) {
|
||||
try {
|
||||
md = (await plugin)(md);
|
||||
} catch {
|
||||
// noop
|
||||
} catch (e) {
|
||||
console.error('Could not load markdown it plugin', e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,18 +136,15 @@ export class MarkdownEngine {
|
||||
alt: ['paragraph', 'reference', 'blockquote', 'list']
|
||||
});
|
||||
|
||||
for (const renderName of ['paragraph_open', 'heading_open', 'image', 'code_block', 'fence', 'blockquote_open', 'list_item_open']) {
|
||||
this.addLineNumberRenderer(md, renderName);
|
||||
}
|
||||
|
||||
this.addImageRenderer(md);
|
||||
this.addFencedRenderer(md);
|
||||
this.addLinkNormalizer(md);
|
||||
this.addLinkValidator(md);
|
||||
this.addNamedHeaders(md);
|
||||
this.addLinkRenderer(md);
|
||||
md.use(pluginSourceMap);
|
||||
return md;
|
||||
});
|
||||
})();
|
||||
}
|
||||
|
||||
const md = await this.md!;
|
||||
@@ -170,7 +192,7 @@ export class MarkdownEngine {
|
||||
};
|
||||
|
||||
const html = engine.renderer.render(tokens, {
|
||||
...(engine as any).options,
|
||||
...engine.options,
|
||||
...config
|
||||
}, env);
|
||||
|
||||
@@ -199,26 +221,9 @@ export class MarkdownEngine {
|
||||
};
|
||||
}
|
||||
|
||||
private addLineNumberRenderer(md: MarkdownIt, ruleName: string): void {
|
||||
const original = md.renderer.rules[ruleName];
|
||||
md.renderer.rules[ruleName] = (tokens: Token[], idx: number, options: any, env: any, self: any) => {
|
||||
const token = tokens[idx];
|
||||
if (token.map && token.map.length) {
|
||||
token.attrSet('data-line', token.map[0] + '');
|
||||
token.attrJoin('class', 'code-line');
|
||||
}
|
||||
|
||||
if (original) {
|
||||
return original(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private addImageRenderer(md: MarkdownIt): void {
|
||||
const original = md.renderer.rules.image;
|
||||
md.renderer.rules.image = (tokens: Token[], idx: number, options: any, env: RenderEnv, self: any) => {
|
||||
md.renderer.rules.image = (tokens: Token[], idx: number, options, env: RenderEnv, self) => {
|
||||
const token = tokens[idx];
|
||||
token.attrJoin('class', 'loading');
|
||||
|
||||
@@ -237,20 +242,24 @@ export class MarkdownEngine {
|
||||
if (original) {
|
||||
return original(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private addFencedRenderer(md: MarkdownIt): void {
|
||||
const original = md.renderer.rules['fenced'];
|
||||
md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options: any, env: any, self: any) => {
|
||||
md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
if (token.map && token.map.length) {
|
||||
token.attrJoin('class', 'hljs');
|
||||
}
|
||||
|
||||
return original(tokens, idx, options, env, self);
|
||||
if (original) {
|
||||
return original(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -282,8 +291,8 @@ export class MarkdownEngine {
|
||||
|
||||
private addNamedHeaders(md: MarkdownIt): void {
|
||||
const original = md.renderer.rules.heading_open;
|
||||
md.renderer.rules.heading_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => {
|
||||
const title = tokens[idx + 1].children.reduce((acc: string, t: any) => acc + t.content, '');
|
||||
md.renderer.rules.heading_open = (tokens: Token[], idx: number, options, env, self) => {
|
||||
const title = tokens[idx + 1].children!.reduce<string>((acc, t) => acc + t.content, '');
|
||||
let slug = this.slugifier.fromHeading(title);
|
||||
|
||||
if (this._slugCount.has(slug.value)) {
|
||||
@@ -294,30 +303,31 @@ export class MarkdownEngine {
|
||||
this._slugCount.set(slug.value, 0);
|
||||
}
|
||||
|
||||
tokens[idx].attrs = tokens[idx].attrs || [];
|
||||
tokens[idx].attrs.push(['id', slug.value]);
|
||||
tokens[idx].attrSet('id', slug.value);
|
||||
|
||||
if (original) {
|
||||
return original(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options, env, self);
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private addLinkRenderer(md: MarkdownIt): void {
|
||||
const old_render = md.renderer.rules.link_open || ((tokens: Token[], idx: number, options: any, _env: any, self: any) => {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
});
|
||||
const original = md.renderer.rules.link_open;
|
||||
|
||||
md.renderer.rules.link_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => {
|
||||
md.renderer.rules.link_open = (tokens: Token[], idx: number, options, env, self) => {
|
||||
const token = tokens[idx];
|
||||
const hrefIndex = token.attrIndex('href');
|
||||
if (hrefIndex >= 0) {
|
||||
const href = token.attrs[hrefIndex][1];
|
||||
token.attrPush(['data-href', href]);
|
||||
const href = token.attrGet('href');
|
||||
// A string, including empty string, may be `href`.
|
||||
if (typeof href === 'string') {
|
||||
token.attrSet('data-href', href);
|
||||
}
|
||||
if (original) {
|
||||
return original(tokens, idx, options, env, self);
|
||||
} else {
|
||||
return self.renderToken(tokens, idx, options);
|
||||
}
|
||||
return old_render(tokens, idx, options, env, self);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -366,7 +376,7 @@ export class MarkdownEngine {
|
||||
}
|
||||
}
|
||||
|
||||
async function getMarkdownOptions(md: () => MarkdownIt) {
|
||||
async function getMarkdownOptions(md: () => MarkdownIt): Promise<MarkdownIt.Options> {
|
||||
const hljs = await import('highlight.js');
|
||||
return {
|
||||
html: true,
|
||||
|
||||
@@ -60,6 +60,10 @@ export class TableOfContentsProvider {
|
||||
const existingSlugEntries = new Map<string, { count: number }>();
|
||||
|
||||
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||||
if (!heading.map) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
|
||||
|
||||
@@ -94,6 +94,17 @@ suite('Markdown Document links', () => {
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
|
||||
|
||||
test('Should navigate to line number within non-md file', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/foo.txt#L3)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'foo.txt'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
|
||||
});
|
||||
|
||||
test('Should navigate to fragment within current file', async () => {
|
||||
await withFileContents(testFileA, joinLines(
|
||||
'[](a#header)',
|
||||
|
||||
@@ -15,7 +15,7 @@ const testFileName = vscode.Uri.file('test.md');
|
||||
suite('markdown.engine', () => {
|
||||
suite('rendering', () => {
|
||||
const input = '# hello\n\nworld!';
|
||||
const output = '<h1 id="hello" data-line="0" class="code-line">hello</h1>\n'
|
||||
const output = '<h1 data-line="0" class="code-line" id="hello">hello</h1>\n'
|
||||
+ '<p data-line="2" class="code-line">world!</p>\n';
|
||||
|
||||
test('Renders a document', async () => {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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 './file';
|
||||
import { extname } from './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.with({ fragment: '' }), 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;
|
||||
}
|
||||
@@ -16,15 +16,15 @@ export class TopmostLineMonitor extends Disposable {
|
||||
|
||||
private readonly pendingUpdates = new Map<string, number>();
|
||||
private readonly throttle = 50;
|
||||
private previousEditorInfo = new Map<string, LastScrollLocation>();
|
||||
public isPrevEditorCustom = false;
|
||||
private previousTextEditorInfo = new Map<string, LastScrollLocation>();
|
||||
private previousStaticEditorInfo = new Map<string, LastScrollLocation>();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
if (vscode.window.activeTextEditor) {
|
||||
const line = getVisibleLine(vscode.window.activeTextEditor);
|
||||
this.setPreviousEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 });
|
||||
this.setPreviousTextEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 });
|
||||
}
|
||||
|
||||
this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => {
|
||||
@@ -32,7 +32,7 @@ export class TopmostLineMonitor extends Disposable {
|
||||
const line = getVisibleLine(event.textEditor);
|
||||
if (typeof line === 'number') {
|
||||
this.updateLine(event.textEditor.document.uri, line);
|
||||
this.setPreviousEditorLine({ uri: event.textEditor.document.uri, line: line });
|
||||
this.setPreviousTextEditorLine({ uri: event.textEditor.document.uri, line: line });
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -41,12 +41,24 @@ export class TopmostLineMonitor extends Disposable {
|
||||
private readonly _onChanged = this._register(new vscode.EventEmitter<{ readonly resource: vscode.Uri, readonly line: number }>());
|
||||
public readonly onDidChanged = this._onChanged.event;
|
||||
|
||||
public setPreviousEditorLine(scrollLocation: LastScrollLocation): void {
|
||||
this.previousEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
|
||||
public setPreviousStaticEditorLine(scrollLocation: LastScrollLocation): void {
|
||||
this.previousStaticEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
|
||||
}
|
||||
|
||||
public getPreviousEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const scrollLoc = this.previousEditorInfo.get(resource.toString());
|
||||
public getPreviousStaticEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const scrollLoc = this.previousStaticEditorInfo.get(resource.toString());
|
||||
this.previousStaticEditorInfo.delete(resource.toString());
|
||||
return scrollLoc?.line;
|
||||
}
|
||||
|
||||
|
||||
public setPreviousTextEditorLine(scrollLocation: LastScrollLocation): void {
|
||||
this.previousTextEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
|
||||
}
|
||||
|
||||
public getPreviousTextEditorLineByUri(resource: vscode.Uri): number | undefined {
|
||||
const scrollLoc = this.previousTextEditorInfo.get(resource.toString());
|
||||
this.previousTextEditorInfo.delete(resource.toString());
|
||||
return scrollLoc?.line;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user