Merge VS Code 1.30.1 (#4092)

This commit is contained in:
Matt Irvine
2019-02-21 17:17:23 -08:00
committed by GitHub
parent a764a481f3
commit 826856c390
11465 changed files with 119542 additions and 255338 deletions

View File

@@ -4,7 +4,6 @@
*--------------------------------------------------------------------------------------------*/
export { OpenDocumentLinkCommand } from './openDocumentLink';
export { OnPreviewStyleLoadErrorCommand } from './onPreviewStyleLoadError';
export { ShowPreviewCommand, ShowPreviewToSideCommand, ShowLockedPreviewToSideCommand } from './showPreview';
export { ShowSourceCommand } from './showSource';
export { RefreshPreviewCommand } from './refreshPreview';

View File

@@ -1,18 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
import * as vscode from 'vscode';
import { Command } from '../commandManager';
export class OnPreviewStyleLoadErrorCommand implements Command {
public readonly id = '_markdown.onPreviewStyleLoadError';
public execute(resources: string[]) {
vscode.window.showWarningMessage(localize('onPreviewStyleLoadError', "Could not load 'markdown.styles': {0}", resources.join(', ')));
}
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import { extname } from 'path';
import { Command } from '../commandManager';
import { MarkdownEngine } from '../markdownEngine';
@@ -35,7 +35,7 @@ export class OpenDocumentLinkCommand implements Command {
public execute(args: OpenDocumentLinkArgs) {
const p = decodeURIComponent(args.path);
return this.tryOpen(p, args).catch(() => {
if (path.extname(p) === '') {
if (p && extname(p) === '') {
return this.tryOpen(p + '.md', args);
}
const resource = vscode.Uri.file(p);
@@ -47,13 +47,14 @@ export class OpenDocumentLinkCommand implements Command {
private async tryOpen(path: string, args: OpenDocumentLinkArgs) {
const resource = vscode.Uri.file(path);
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document) && vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
return this.tryRevealLine(vscode.window.activeTextEditor, args.fragment);
} else {
return vscode.workspace.openTextDocument(resource)
.then(vscode.window.showTextDocument)
.then(editor => this.tryRevealLine(editor, args.fragment));
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
if (!path || vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
return this.tryRevealLine(vscode.window.activeTextEditor, args.fragment);
}
}
return vscode.workspace.openTextDocument(resource)
.then(vscode.window.showTextDocument)
.then(editor => this.tryRevealLine(editor, args.fragment));
}
private async tryRevealLine(editor: vscode.TextEditor, fragment?: string) {
@@ -73,3 +74,37 @@ export class OpenDocumentLinkCommand implements Command {
}
}
}
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

@@ -38,9 +38,10 @@ async function showPreview(
return;
}
const resourceColumn = (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One;
webviewManager.preview(resource, {
resourceColumn: (vscode.window.activeTextEditor && vscode.window.activeTextEditor.viewColumn) || vscode.ViewColumn.One,
previewColumn: previewSettings.sideBySide ? vscode.ViewColumn.Beside : vscode.ViewColumn.Active,
resourceColumn: resourceColumn,
previewColumn: previewSettings.sideBySide ? resourceColumn + 1 : resourceColumn,
locked: !!previewSettings.locked
});

View File

@@ -56,7 +56,6 @@ export function activate(context: vscode.ExtensionContext) {
commandManager.register(new commands.RefreshPreviewCommand(previewManager));
commandManager.register(new commands.MoveCursorToPositionCommand());
commandManager.register(new commands.ShowPreviewSecuritySelectorCommand(previewSecuritySelector, previewManager));
commandManager.register(new commands.OnPreviewStyleLoadErrorCommand());
commandManager.register(new commands.OpenDocumentLinkCommand(engine));
commandManager.register(new commands.ToggleLockCommand(previewManager));
// {{SQL CARBON EDIT}}

View File

@@ -3,34 +3,38 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import * as path from 'path';
import * as vscode from 'vscode';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
import { getUriForLinkWithKnownExternalScheme } from '../util/links';
function normalizeLink(
document: vscode.TextDocument,
link: string,
base: string
): vscode.Uri {
const uri = vscode.Uri.parse(link);
if (uri.scheme) {
return uri;
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link);
if (externalSchemeUri) {
return externalSchemeUri;
}
// assume it must be a file
let resourcePath = uri.path;
if (!uri.path) {
// Assume it must be an relative or absolute file path
// Use a fake scheme to avoid parse warnings
const tempUri = vscode.Uri.parse(`vscode-resource:${link}`);
let resourcePath = tempUri.path;
if (!tempUri.path && document.uri.scheme === 'file') {
resourcePath = document.uri.path;
} else if (uri.path[0] === '/') {
} else if (tempUri.path[0] === '/') {
const root = vscode.workspace.getWorkspaceFolder(document.uri);
if (root) {
resourcePath = path.join(root.uri.fsPath, uri.path);
resourcePath = path.join(root.uri.fsPath, tempUri.path);
}
} else {
resourcePath = path.join(base, uri.path);
resourcePath = base ? path.join(base, tempUri.path) : tempUri.path;
}
return OpenDocumentLinkCommand.createCommandUri(resourcePath, uri.fragment);
return OpenDocumentLinkCommand.createCommandUri(resourcePath, tempUri.fragment);
}
function matchAll(
@@ -55,7 +59,7 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
document: vscode.TextDocument,
_token: vscode.CancellationToken
): vscode.DocumentLink[] {
const base = path.dirname(document.uri.fsPath);
const base = document.uri.scheme === 'file' ? path.dirname(document.uri.fsPath) : '';
const text = document.getText();
return this.providerInlineLinks(text, document, base)

View File

@@ -3,8 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Token } from 'markdown-it';
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContentsProvider } from '../tableOfContentsProvider';
@@ -16,35 +16,83 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
private readonly engine: MarkdownEngine
) { }
private async getRegions(document: vscode.TextDocument): Promise<vscode.FoldingRange[]> {
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 tokens = await this.engine.parse(document.uri, document.getText());
const regionMarkers = tokens.filter(isRegionMarker)
.map(token => ({ line: token.map[0], isStart: isStartRegion(token.content) }));
const nestingStack: { line: number, isStart: boolean }[] = [];
return regionMarkers
.map(marker => {
if (marker.isStart) {
nestingStack.push(marker);
} else if (nestingStack.length && nestingStack[nestingStack.length - 1].isStart) {
return new vscode.FoldingRange(nestingStack.pop()!.line, marker.line, vscode.FoldingRangeKind.Region);
} else {
// noop: invalid nesting (i.e. [end, start] or [start, end, end])
}
return null;
})
.filter((region: vscode.FoldingRange | null): region is vscode.FoldingRange => !!region);
}
public async provideFoldingRanges(
document: vscode.TextDocument,
_: vscode.FoldingContext,
_token: vscode.CancellationToken
): Promise<vscode.FoldingRange[]> {
const tocProvider = new TableOfContentsProvider(this.engine, document);
let toc = await tocProvider.getToc();
if (toc.length > rangeLimit) {
toc = toc.slice(0, rangeLimit);
}
const foldingRanges = toc.map((entry, startIndex) => {
const start = entry.line;
let end: number | undefined = undefined;
for (let i = startIndex + 1; i < toc.length; ++i) {
if (toc[i].level <= entry.level) {
end = toc[i].line - 1;
if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) {
end = end - 1;
}
break;
}
}
return new vscode.FoldingRange(
start,
typeof end === 'number' ? end : document.lineCount - 1);
});
return foldingRanges;
const foldables = await Promise.all([
this.getRegions(document),
this.getHeaderFoldingRanges(document),
this.getBlockFoldingRanges(document)]);
return ([] as vscode.FoldingRange[]).concat.apply([], foldables).slice(0, rangeLimit);
}
}
private async getHeaderFoldingRanges(document: vscode.TextDocument) {
const tocProvider = new TableOfContentsProvider(this.engine, document);
const toc = await tocProvider.getToc();
return toc.map(entry => {
let endLine = entry.location.range.end.line;
if (document.lineAt(endLine).isEmptyOrWhitespace && endLine >= entry.line + 1) {
endLine = endLine - 1;
}
return new vscode.FoldingRange(entry.line, endLine);
});
}
private async getBlockFoldingRanges(document: vscode.TextDocument): Promise<vscode.FoldingRange[]> {
const isFoldableToken = (token: Token) => {
switch (token.type) {
case 'fence':
case 'list_item_open':
return token.map[1] > token.map[0];
case 'html_block':
return token.map[1] > token.map[0] + 1;
default:
return false;
}
};
const tokens = await this.engine.parse(document.uri, document.getText());
const multiLineListItems = tokens.filter(isFoldableToken);
return multiLineListItems.map(listItem => {
const start = listItem.map[0];
let end = listItem.map[1] - 1;
if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) {
end = end - 1;
}
return new vscode.FoldingRange(start, end);
});
}
}

View File

@@ -15,8 +15,51 @@ import { getVisibleLine, MarkdownFileTopmostLineMonitor } from '../util/topmostL
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContributions } from '../markdownExtensions';
import { isMarkdownFile } from '../util/file';
import { resolveLinkToMarkdownFile } from '../commands/openDocumentLink';
const localize = nls.loadMessageBundle();
interface WebviewMessage {
readonly source: string;
}
interface CacheImageSizesMessage extends WebviewMessage {
readonly type: 'cacheImageSizes';
readonly body: { id: string, width: number, height: number }[];
}
interface RevealLineMessage extends WebviewMessage {
readonly type: 'revealLine';
readonly body: {
readonly line: number;
};
}
interface DidClickMessage extends WebviewMessage {
readonly type: 'didClick';
readonly body: {
readonly line: number;
};
}
interface ClickLinkMessage extends WebviewMessage {
readonly type: 'clickLink';
readonly body: {
readonly path: string;
readonly fragment?: string;
};
}
interface ShowPreviewSecuritySelectorMessage extends WebviewMessage {
readonly type: 'showPreviewSecuritySelector';
}
interface PreviewStyleLoadErrorMessage extends WebviewMessage {
readonly type: 'previewStyleLoadError';
readonly body: {
readonly unloadedStyles: string[];
};
}
export class MarkdownPreview {
public static viewType = 'markdown.preview';
@@ -33,6 +76,7 @@ export class MarkdownPreview {
private forceUpdate = false;
private isScrolling = false;
private _disposed: boolean = false;
private imageInfo: { id: string, width: number, height: number }[] = [];
public static async revive(
webview: vscode.WebviewPanel,
@@ -117,14 +161,14 @@ export class MarkdownPreview {
this._onDidChangeViewStateEmitter.fire(e);
}, null, this.disposables);
this.editor.webview.onDidReceiveMessage(e => {
this.editor.webview.onDidReceiveMessage((e: CacheImageSizesMessage | RevealLineMessage | DidClickMessage | ClickLinkMessage | ShowPreviewSecuritySelectorMessage | PreviewStyleLoadErrorMessage) => {
if (e.source !== this._resource.toString()) {
return;
}
switch (e.type) {
case 'command':
vscode.commands.executeCommand(e.body.command, ...e.body.args);
case 'cacheImageSizes':
this.onCacheImageSizes(e.body);
break;
case 'revealLine':
@@ -135,6 +179,17 @@ export class MarkdownPreview {
this.onDidClickPreview(e.body.line);
break;
case 'clickLink':
this.onDidClickPreviewLink(e.body.path, e.body.fragment);
break;
case 'showPreviewSecuritySelector':
vscode.commands.executeCommand('markdown.showPreviewSecuritySelector', e.source);
break;
case 'previewStyleLoadError':
vscode.window.showWarningMessage(localize('onPreviewStyleLoadError', "Could not load 'markdown.styles': {0}", e.body.unloadedStyles.join(', ')));
break;
}
}, null, this.disposables);
@@ -181,7 +236,8 @@ export class MarkdownPreview {
return {
resource: this.resource.toString(),
locked: this._locked,
line: this.line
line: this.line,
imageInfo: this.imageInfo
};
}
@@ -347,7 +403,6 @@ export class MarkdownPreview {
): vscode.WebviewOptions {
return {
enableScripts: true,
enableCommandUris: true,
localResourceRoots: MarkdownPreview.getLocalResourceRoots(resource, contributions)
};
}
@@ -400,6 +455,24 @@ export class MarkdownPreview {
vscode.workspace.openTextDocument(this._resource).then(vscode.window.showTextDocument);
}
private async onDidClickPreviewLink(path: string, fragment: string | undefined) {
const config = vscode.workspace.getConfiguration('markdown', this.resource);
const openLinks = config.get<string>('preview.openMarkdownLinks', 'inPreview');
if (openLinks === 'inPreview') {
const markdownLink = await resolveLinkToMarkdownFile(path);
if (markdownLink) {
this.update(markdownLink);
return;
}
}
vscode.commands.executeCommand('_markdown.openDocumentLink', { path, fragment });
}
private async onCacheImageSizes(imageInfo: { id: string, width: number, height: number }[]) {
this.imageInfo = imageInfo;
}
}
export interface PreviewSettings {

View File

@@ -79,7 +79,7 @@ export class MarkdownContentProvider {
data-strings="${JSON.stringify(previewStrings).replace(/"/g, '&quot;')}"
data-state="${JSON.stringify(state || {}).replace(/"/g, '&quot;')}">
<script src="${this.extensionResourcePath('pre.js')}" nonce="${nonce}"></script>
${this.getStyles(sourceUri, nonce, config)}
${this.getStyles(sourceUri, nonce, config, state)}
<base href="${markdownDocument.uri.with({ scheme: 'vscode-resource' }).toString(true)}">
</head>
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
@@ -104,7 +104,7 @@ export class MarkdownContentProvider {
// Use href if it is already an URL
const hrefUri = vscode.Uri.parse(href);
if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) {
return hrefUri.toString();
return hrefUri.toString(true);
}
// Use href as file URI if it is absolute
@@ -131,7 +131,7 @@ export class MarkdownContentProvider {
private computeCustomStyleSheetIncludes(resource: vscode.Uri, config: MarkdownPreviewConfiguration): string {
if (Array.isArray(config.styles)) {
return config.styles.map(style => {
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '&quot;')}" href="${this.fixHref(resource, style)}" type="text/css" media="screen">`;
return `<link rel="stylesheet" class="code-user-style" data-source="${style.replace(/"/g, '&quot;')}" href="${this.fixHref(resource, style).replace(/"/g, '&quot;')}" type="text/css" media="screen">`;
}).join('\n');
}
return '';
@@ -147,14 +147,30 @@ export class MarkdownContentProvider {
</style>`;
}
private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration): string {
private getImageStabilizerStyles(state?: any) {
let ret = '<style>\n';
if (state && state.imageInfo) {
state.imageInfo.forEach((imgInfo: any) => {
ret += `#${imgInfo.id}.loading {
height: ${imgInfo.height}px;
width: ${imgInfo.width}px;
}\n`;
});
}
ret += '</style>\n';
return ret;
}
private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration, state?: any): string {
const baseStyles = this.contributions.previewStyles
.map(resource => `<link rel="stylesheet" type="text/css" href="${resource.toString()}">`)
.join('\n');
return `${baseStyles}
${this.getSettingsOverrideStyles(nonce, config)}
${this.computeCustomStyleSheetIncludes(resource, config)}`;
${this.computeCustomStyleSheetIncludes(resource, config)}
${this.getImageStabilizerStyles(state)}`;
}
private getScripts(nonce: string): string {

View File

@@ -6,8 +6,10 @@
import { MarkdownIt, Token } from 'markdown-it';
import * as path from 'path';
import * as vscode from 'vscode';
import * as crypto from 'crypto';
import { MarkdownContributions } from './markdownExtensions';
import { Slugifier } from './slugify';
import { getUriForLinkWithKnownExternalScheme } from './util/links';
const FrontMatterRegex = /^---\s*[^]*?(-{3}|\.{3})\s*/;
@@ -15,8 +17,8 @@ export class MarkdownEngine {
private md?: MarkdownIt;
private firstLine?: number;
private currentDocument?: vscode.Uri;
private _slugCount = new Map<string, number>();
public constructor(
private readonly extensionPreviewResourceProvider: MarkdownContributions,
@@ -34,14 +36,19 @@ export class MarkdownEngine {
private async getEngine(resource: vscode.Uri): Promise<MarkdownIt> {
if (!this.md) {
const hljs = await import('highlight.js');
const mdnh = await import('markdown-it-named-headers');
this.md = (await import('markdown-it'))({
html: true,
highlight: (str: string, lang: string) => {
highlight: (str: string, lang?: string) => {
// Workaround for highlight not supporting tsx: https://github.com/isagalaev/highlight.js/issues/1155
if (lang && ['tsx', 'typescriptreact'].indexOf(lang.toLocaleLowerCase()) >= 0) {
lang = 'jsx';
}
if (lang && lang.toLocaleLowerCase() === 'json5') {
lang = 'json';
}
if (lang && lang.toLocaleLowerCase() === 'c#') {
lang = 'cs';
}
if (lang && hljs.getLanguage(lang)) {
try {
return `<div>${hljs.highlight(lang, str, true).value}</div>`;
@@ -49,8 +56,6 @@ export class MarkdownEngine {
}
return `<code><div>${this.md!.utils.escapeHtml(str)}</div></code>`;
}
}).use(mdnh, {
slugify: (header: string) => this.slugifier.fromHeading(header).value
});
for (const plugin of this.extensionPreviewResourceProvider.markdownItPlugins) {
@@ -61,10 +66,12 @@ export class MarkdownEngine {
this.addLineNumberRenderer(this.md, renderName);
}
this.addImageStabilizer(this.md);
this.addFencedRenderer(this.md);
this.addLinkNormalizer(this.md);
this.addLinkValidator(this.md);
this.addNamedHeaders(this.md);
}
const config = vscode.workspace.getConfiguration('markdown', resource);
@@ -101,18 +108,24 @@ export class MarkdownEngine {
}
this.currentDocument = document;
this.firstLine = offset;
this._slugCount = new Map<string, number>();
const engine = await this.getEngine(document);
return engine.render(text);
}
public async parse(document: vscode.Uri, source: string): Promise<Token[]> {
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
const { text, offset } = this.stripFrontmatter(source);
this.currentDocument = document;
this._slugCount = new Map<string, number>();
const engine = await this.getEngine(document);
return engine.parse(text, {}).map(token => {
return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {}).map(token => {
if (token.map) {
token.map[0] += offset;
token.map[1] += offset;
}
return token;
});
@@ -135,6 +148,28 @@ export class MarkdownEngine {
};
}
private addImageStabilizer(md: any): void {
const original = md.renderer.rules.image;
md.renderer.rules.image = (tokens: any, idx: number, options: any, env: any, self: any) => {
const token = tokens[idx];
token.attrJoin('class', 'loading');
const src = token.attrGet('src');
if (src) {
const hash = crypto.createHash('sha256');
hash.update(src);
const imgHash = hash.digest('hex');
token.attrSet('id', `image-hash-${imgHash}`);
}
if (original) {
return original(tokens, idx, options, env, self);
} else {
return self.renderToken(tokens, idx, options, env, self);
}
};
}
private addFencedRenderer(md: any): void {
const original = md.renderer.rules['fenced'];
md.renderer.rules['fenced'] = (tokens: any, idx: number, options: any, env: any, self: any) => {
@@ -151,8 +186,18 @@ export class MarkdownEngine {
const normalizeLink = md.normalizeLink;
md.normalizeLink = (link: string) => {
try {
let uri = vscode.Uri.parse(link);
if (!uri.scheme && uri.path) {
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(link);
if (externalSchemeUri) {
// set true to skip encoding
return normalizeLink(externalSchemeUri.toString(true));
}
// Assume it must be an relative or absolute file path
// Use a fake scheme to avoid parse warnings
let uri = vscode.Uri.parse(`vscode-resource:${link}`);
if (uri.path) {
// Assume it must be a file
const fragment = uri.fragment;
if (uri.path[0] === '/') {
@@ -170,10 +215,8 @@ export class MarkdownEngine {
});
}
return normalizeLink(uri.with({ scheme: 'vscode-resource' }).toString(true));
} else if (!uri.scheme && !uri.path && uri.fragment) {
return normalizeLink(uri.with({
fragment: this.slugifier.fromHeading(uri.fragment).value
}).toString(true));
} else if (!uri.path && uri.fragment) {
return `#${this.slugifier.fromHeading(uri.fragment).value}`;
}
} catch (e) {
// noop
@@ -189,4 +232,29 @@ export class MarkdownEngine {
return validateLink(link) || link.indexOf('file:') === 0;
};
}
private addNamedHeaders(md: any): void {
const original = md.renderer.rules.heading_open;
md.renderer.rules.heading_open = (tokens: any, idx: number, options: any, env: any, self: any) => {
const title = tokens[idx + 1].children.reduce((acc: string, t: any) => acc + t.content, '');
let slug = this.slugifier.fromHeading(title);
if (this._slugCount.has(slug.value)) {
const count = this._slugCount.get(slug.value)!;
this._slugCount.set(slug.value, count + 1);
slug = this.slugifier.fromHeading(slug.value + '-' + (count + 1));
} else {
this._slugCount.set(slug.value, 0);
}
tokens[idx].attrs = tokens[idx].attrs || [];
tokens[idx].attrs.push(['id', slug.value]);
if (original) {
return original(tokens, idx, options, env, self);
} else {
return self.renderToken(tokens, idx, options, env, self);
}
};
}
}

View File

@@ -11,7 +11,7 @@ import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
export enum MarkdownPreviewSecurityLevel {
export const enum MarkdownPreviewSecurityLevel {
Strict = 0,
AllowInsecureContent = 1,
AllowScriptsAndAllContent = 2,

View File

@@ -23,7 +23,7 @@ export const githubSlugifier: Slugifier = new class implements Slugifier {
heading.trim()
.toLowerCase()
.replace(/\s+/g, '-') // Replace whitespace with -
.replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known puctuators
.replace(/[\]\[\!\'\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\\\^\_\{\|\}\~\`。,、;:?!…—·ˉ¨‘’“”々~‖∶"'`|〃〔〕〈〉《》「」『』.〖〗【】()[]{}]/g, '') // Remove known punctuators
.replace(/^\-+/, '') // Remove leading -
.replace(/\-+$/, '') // Remove trailing -
);

View File

@@ -17,6 +17,7 @@ export interface TocEntry {
export interface SkinnyTextDocument {
readonly uri: vscode.Uri;
readonly lineCount: number;
getText(): string;
lineAt(line: number): vscode.TextLine;
}
@@ -50,18 +51,48 @@ export class TableOfContentsProvider {
const toc: TocEntry[] = [];
const tokens = await this.engine.parse(document.uri, document.getText());
const slugCount = new Map<string, number>();
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
const lineNumber = heading.map[0];
const line = document.lineAt(lineNumber);
let slug = githubSlugifier.fromHeading(line.text);
if (slugCount.has(slug.value)) {
const count = slugCount.get(slug.value)!;
slugCount.set(slug.value, count + 1);
slug = githubSlugifier.fromHeading(slug.value + '-' + (count + 1));
} else {
slugCount.set(slug.value, 0);
}
toc.push({
slug: githubSlugifier.fromHeading(line.text),
slug,
text: TableOfContentsProvider.getHeaderText(line.text),
level: TableOfContentsProvider.getHeaderLevel(heading.markup),
line: lineNumber,
location: new vscode.Location(document.uri, line.range)
});
}
return toc;
// Get full range of section
return toc.map((entry, startIndex): TocEntry => {
let end: number | undefined = undefined;
for (let i = startIndex + 1; i < toc.length; ++i) {
if (toc[i].level <= entry.level) {
end = toc[i].line - 1;
break;
}
}
const endLine = typeof end === 'number' ? end : document.lineCount - 1;
return {
...entry,
location: new vscode.Location(document.uri,
new vscode.Range(
entry.location.range.start,
new vscode.Position(endLine, document.lineAt(endLine).range.end.character)))
};
});
}
private static getHeaderLevel(markup: string): number {

View File

@@ -10,7 +10,7 @@ import LinkProvider from '../features/documentLinkProvider';
import { InMemoryDocument } from './inMemoryDocument';
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
const noopToken = new class implements vscode.CancellationToken {
private _onCancellationRequestedEmitter = new vscode.EventEmitter<void>();

View File

@@ -11,7 +11,7 @@ import { InMemoryDocument } from './inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
function getSymbolsForFile(fileContents: string) {
@@ -82,5 +82,16 @@ suite('markdown.DocumentSymbolProvider', () => {
assert.strictEqual(symbols[0].children[0].name, '### h2');
assert.strictEqual(symbols[0].children[1].name, '## h3');
});
test('Should handle line separator in file. Issue #63749', async () => {
const symbols = await getSymbolsForFile(`# A
- foo
# B
- bar`);
assert.strictEqual(symbols.length, 2);
assert.strictEqual(symbols[0].name, '# A');
assert.strictEqual(symbols[1].name, '# B');
});
});

View File

@@ -11,7 +11,7 @@ import MarkdownFoldingProvider from '../features/foldingProvider';
import { InMemoryDocument } from './inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
suite('markdown.FoldingProvider', () => {
test('Should not return anything for empty document', async () => {
@@ -78,6 +78,103 @@ y`);
assert.strictEqual(firstFold.end, 2);
});
test('Should fold nested <!-- #region --> markers', async () => {
const folds = await getFoldsForDocument(`a
<!-- #region -->
b
<!-- #region hello!-->
b.a
<!-- #endregion -->
b
<!-- #region: foo! -->
b.b
<!-- #endregion: foo -->
b
<!-- #endregion -->
a`);
assert.strictEqual(folds.length, 3);
const [outer, first, second] = folds.sort((a, b) => a.start - b.start);
assert.strictEqual(outer.start, 1);
assert.strictEqual(outer.end, 11);
assert.strictEqual(first.start, 3);
assert.strictEqual(first.end, 5);
assert.strictEqual(second.start, 7);
assert.strictEqual(second.end, 9);
});
test('Should fold from list to end of document', async () => {
const folds = await getFoldsForDocument(`a
- b
c
d`);
assert.strictEqual(folds.length, 1);
const firstFold = folds[0];
assert.strictEqual(firstFold.start, 1);
assert.strictEqual(firstFold.end, 3);
});
test('lists folds should span multiple lines of content', async () => {
const folds = await getFoldsForDocument(`a
- This list item\n spans multiple\n lines.`);
assert.strictEqual(folds.length, 1);
const firstFold = folds[0];
assert.strictEqual(firstFold.start, 1);
assert.strictEqual(firstFold.end, 3);
});
test('List should leave single blankline before new element', async () => {
const folds = await getFoldsForDocument(`- a
a
b`);
assert.strictEqual(folds.length, 1);
const firstFold = folds[0];
assert.strictEqual(firstFold.start, 0);
assert.strictEqual(firstFold.end, 3);
});
test('Should fold fenced code blocks', async () => {
const folds = await getFoldsForDocument(`~~~ts
a
~~~
b`);
assert.strictEqual(folds.length, 1);
const firstFold = folds[0];
assert.strictEqual(firstFold.start, 0);
assert.strictEqual(firstFold.end, 2);
});
test('Should fold fenced code blocks with yaml front matter', async () => {
const folds = await getFoldsForDocument(`---
title: bla
---
~~~ts
a
~~~
a
a
b
a`);
assert.strictEqual(folds.length, 1);
const firstFold = folds[0];
assert.strictEqual(firstFold.start, 4);
assert.strictEqual(firstFold.end, 6);
});
test('Should fold html blocks', async () => {
const folds = await getFoldsForDocument(`x
<div>
fa
</div>`);
assert.strictEqual(folds.length, 1);
const firstFold = folds[0];
assert.strictEqual(firstFold.start, 1);
assert.strictEqual(firstFold.end, 3);
});
});

View File

@@ -11,7 +11,7 @@ import { TableOfContentsProvider } from '../tableOfContentsProvider';
import { InMemoryDocument } from './inMemoryDocument';
import { createNewMarkdownEngine } from './engine';
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
suite('markdown.TableOfContentsProvider', () => {
test('Lookup should not return anything for empty document', async () => {
@@ -106,4 +106,25 @@ suite('markdown.TableOfContentsProvider', () => {
assert.strictEqual((await provider.lookup('Заголовок-header-3'))!.line, 4);
assert.strictEqual((await provider.lookup('Заголовок'))!.line, 5);
});
test('Lookup should support suffixes for repeated headers', async () => {
const doc = new InMemoryDocument(testFileName, `# a\n# a\n## a`);
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
{
const entry = await provider.lookup('a');
assert.ok(entry);
assert.strictEqual(entry!.line, 0);
}
{
const entry = await provider.lookup('a-1');
assert.ok(entry);
assert.strictEqual(entry!.line, 1);
}
{
const entry = await provider.lookup('a-2');
assert.ok(entry);
assert.strictEqual(entry!.line, 2);
}
});
});

View File

@@ -22,7 +22,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
});
test('Should return symbols from workspace with one markdown file', async () => {
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([
new InMemoryDocument(testFileName, `# header1\nabc\n## header2`)
@@ -49,7 +49,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
});
test('Should update results when markdown file changes symbols', async () => {
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
new InMemoryDocument(testFileName, `# header1`)
@@ -68,7 +68,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
});
test('Should remove results when file is deleted', async () => {
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
new InMemoryDocument(testFileName, `# header1`)
@@ -84,7 +84,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
});
test('Should update results when markdown file is created', async () => {
const testFileName = vscode.Uri.parse('test.md');
const testFileName = vscode.Uri.file('test.md');
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
new InMemoryDocument(testFileName, `# header1`)
@@ -94,7 +94,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
// Creat file
workspaceFileProvider.createDocument(new InMemoryDocument(vscode.Uri.parse('test2.md'), `# new header\nabc\n## header2`));
workspaceFileProvider.createDocument(new InMemoryDocument(vscode.Uri.file('test2.md'), `# new header\nabc\n## header2`));
const newSymbols = await provider.provideWorkspaceSymbols('');
assert.strictEqual(newSymbols.length, 3);
});
@@ -105,7 +105,7 @@ class InMemoryWorkspaceMarkdownDocumentProvider implements WorkspaceMarkdownDocu
private readonly _documents = new Map<string, vscode.TextDocument>();
constructor(documents: vscode.TextDocument[]) {
for( const doc of documents) {
for (const doc of documents) {
this._documents.set(doc.fileName, doc);
}
}

View File

@@ -1,5 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
declare module 'markdown-it-named-headers' { }

View File

@@ -4,5 +4,4 @@
*--------------------------------------------------------------------------------------------*/
/// <reference path='../../../../src/vs/vscode.d.ts'/>
/// <reference path='../../../../src/vs/vscode.proposed.d.ts'/>
/// <reference types='@types/node'/>

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
const knownSchemes = ['http:', 'https:', 'file:', 'mailto:', 'data:', 'vscode-resource:'];
export function getUriForLinkWithKnownExternalScheme(
link: string,
): vscode.Uri | undefined {
if (knownSchemes.some(knownScheme => link.toLowerCase().startsWith(knownScheme))) {
return vscode.Uri.parse(link);
}
return undefined;
}