Merge from vscode e3c4990c67c40213af168300d1cfeb71d680f877 (#16569)

This commit is contained in:
Cory Rivera
2021-08-25 16:28:29 -07:00
committed by GitHub
parent ab1112bfb3
commit cb7b7da0a4
1752 changed files with 59525 additions and 33878 deletions

View File

@@ -1,6 +1,6 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const path = require('path');
const esbuild = require('esbuild');

File diff suppressed because one or more lines are too long

View File

@@ -106,6 +106,13 @@ body.showEditorSelection li.code-line:hover:before {
border-left: none;
}
ul ul,
ul ol,
ol ul,
ol ol {
margin-bottom: 0;
}
img {
max-width: 100%;
max-height: 100%;
@@ -152,6 +159,7 @@ h1 {
table {
border-collapse: collapse;
margin-bottom: 0.7em;
}
th {

View File

@@ -1 +0,0 @@
!function(e){var t={};function n(o){if(t[o])return t[o].exports;var s=t[o]={i:o,l:!1,exports:{}};return e[o].call(s.exports,s,s.exports,n),s.l=!0,s.exports}n.m=e,n.c=t,n.d=function(e,t,o){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:o})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var o=Object.create(null);if(n.r(o),Object.defineProperty(o,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)n.d(o,s,function(t){return e[t]}.bind(null,s));return o},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=11)}([function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getSettings=t.getData=void 0;let o=void 0;function s(e){const t=document.getElementById("vscode-markdown-preview-data");if(t){const n=t.getAttribute(e);if(n)return JSON.parse(n)}throw new Error("Could not load data for "+e)}t.getData=s,t.getSettings=function(){if(o)return o;if(o=s("data-settings"),o)return o;throw new Error("Could not load settings")}},,,,,,,,,,,function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0});const o=n(12),s=n(14);window.cspAlerter=new o.CspAlerter,window.styleLoadingMonitor=new s.StyleLoadingMonitor},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.CspAlerter=void 0;const o=n(0),s=n(13);t.CspAlerter=class{constructor(){this.didShow=!1,this.didHaveCspWarning=!1,document.addEventListener("securitypolicyviolation",()=>{this.onCspWarning()}),window.addEventListener("message",e=>{e&&e.data&&"vscode-did-block-svg"===e.data.name&&this.onCspWarning()})}setPoster(e){this.messaging=e,this.didHaveCspWarning&&this.showCspWarning()}onCspWarning(){this.didHaveCspWarning=!0,this.showCspWarning()}showCspWarning(){const e=(0,s.getStrings)(),t=(0,o.getSettings)();if(this.didShow||t.disableSecurityWarnings||!this.messaging)return;this.didShow=!0;const n=document.createElement("a");n.innerText=e.cspAlertMessageText,n.setAttribute("id","code-csp-warning"),n.setAttribute("title",e.cspAlertMessageTitle),n.setAttribute("role","button"),n.setAttribute("aria-label",e.cspAlertMessageLabel),n.onclick=()=>{this.messaging.postMessage("showPreviewSecuritySelector",{source:t.source})},document.body.appendChild(n)}}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getStrings=void 0,t.getStrings=function(){const e=document.getElementById("vscode-markdown-preview-data");if(e){const t=e.getAttribute("data-strings");if(t)return JSON.parse(t)}throw new Error("Could not load strings")}},function(e,t,n){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.StyleLoadingMonitor=void 0;t.StyleLoadingMonitor=class{constructor(){this.unloadedStyles=[],this.finishedLoading=!1;const e=e=>{const t=e.target.dataset.source;this.unloadedStyles.push(t)};window.addEventListener("DOMContentLoaded",()=>{for(const t of document.getElementsByClassName("code-user-style"))t.dataset.source&&(t.onerror=e)}),window.addEventListener("load",()=>{this.unloadedStyles.length&&(this.finishedLoading=!0,this.poster&&this.poster.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles}))})}setPoster(e){this.poster=e,this.finishedLoading&&e.postMessage("previewStyleLoadError",{unloadedStyles:this.unloadedStyles})}}}]);

View File

@@ -1,39 +1,177 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const MarkdownIt = require('markdown-it');
export async function activate(ctx: {
dependencies: ReadonlyArray<{ entrypoint: string }>
}) {
export function activate() {
let markdownIt = new MarkdownIt({
html: true
});
// Should we load the deps before this point?
// Also could we await inside `renderMarkup`?
await Promise.all(ctx.dependencies.map(async (dep) => {
try {
const api = await import(dep.entrypoint);
if (api?.extendMarkdownIt) {
markdownIt = api.extendMarkdownIt(markdownIt);
}
} catch (e) {
console.error('Could not load markdown entryPoint', e);
const style = document.createElement('style');
style.classList.add('markdown-style');
style.textContent = `
.emptyMarkdownCell::before {
content: "${document.documentElement.style.getPropertyValue('--notebook-cell-markup-empty-content')}";
font-style: italic;
opacity: 0.6;
}
}));
img {
max-width: 100%;
max-height: 100%;
}
a {
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
a:focus,
input:focus,
select:focus,
textarea:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
hr {
border: 0;
height: 2px;
border-bottom: 2px solid;
}
h1 {
font-size: 26px;
line-height: 31px;
margin: 0;
margin-bottom: 13px;
}
h2 {
font-size: 19px;
margin: 0;
margin-bottom: 10px;
}
h1,
h2,
h3 {
font-weight: normal;
}
div {
width: 100%;
}
/* Adjust margin of first item in markdown cell */
*:first-child {
margin-top: 0px;
}
/* h1 tags don't need top margin */
h1:first-child {
margin-top: 0;
}
/* Removes bottom margin when only one item exists in markdown cell */
*:only-child,
*:last-child {
margin-bottom: 0;
padding-bottom: 0;
}
/* makes all markdown cells consistent */
div {
min-height: var(--notebook-markdown-min-height);
}
table {
border-collapse: collapse;
border-spacing: 0;
}
table th,
table td {
border: 1px solid;
}
table > thead > tr > th {
text-align: left;
border-bottom: 1px solid;
}
table > thead > tr > th,
table > thead > tr > td,
table > tbody > tr > th,
table > tbody > tr > td {
padding: 5px 10px;
}
table > tbody > tr + tr > td {
border-top: 1px solid;
}
blockquote {
margin: 0 7px 0 5px;
padding: 0 16px 0 10px;
border-left-width: 5px;
border-left-style: solid;
}
code,
.code {
font-size: 1em;
line-height: 1.357em;
}
.code {
white-space: pre-wrap;
}
`;
document.head.append(style);
return {
renderMarkup: (context: { element: HTMLElement, content: string }) => {
const rendered = markdownIt.render(context.content);
context.element.innerHTML = rendered;
renderOutputItem: (outputInfo: { text(): string }, element: HTMLElement) => {
let previewNode: HTMLElement;
if (!element.shadowRoot) {
const previewRoot = element.attachShadow({ mode: 'open' });
// Insert styles into markdown preview shadow dom so that they are applied
for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) {
context.element.appendChild(markdownStyleNode.cloneNode(true));
// Insert styles into markdown preview shadow dom so that they are applied.
// First add default webview style
const defaultStyles = document.getElementById('_defaultStyles') as HTMLStyleElement;
previewRoot.appendChild(defaultStyles.cloneNode(true));
// And then contributed styles
for (const markdownStyleNode of document.getElementsByClassName('markdown-style')) {
previewRoot.appendChild(markdownStyleNode.cloneNode(true));
}
previewNode = document.createElement('div');
previewNode.id = 'preview';
previewRoot.appendChild(previewNode);
} else {
previewNode = element.shadowRoot.getElementById('preview')!;
}
const text = outputInfo.text();
if (text.trim().length === 0) {
previewNode.innerText = '';
previewNode.classList.add('emptyMarkdownCell');
} else {
previewNode.classList.remove('emptyMarkdownCell');
const rendered = markdownIt.render(text);
previewNode.innerHTML = rendered;
}
},
extendMarkdownIt: (f: (md: typeof markdownIt) => void) => {
f(markdownIt);
}
};
}

View File

@@ -26,6 +26,7 @@
"onCommand:markdown.showSource",
"onCommand:markdown.showPreviewSecuritySelector",
"onCommand:markdown.api.render",
"onCommand:markdown.api.reloadPlugins",
"onWebviewPanel:markdown.preview",
"onCustomEditor:vscode.markdown.preview.editor"
],
@@ -40,7 +41,7 @@
}
},
"contributes": {
"notebookMarkupRenderers": [
"notebookRenderer": [
{
"id": "markdownItRenderer",
"displayName": "Markdown it renderer",
@@ -361,7 +362,8 @@
"@types/highlight.js": "10.1.0",
"@types/lodash.throttle": "^4.1.3",
"@types/markdown-it": "0.0.2",
"@types/node": "^12.19.9",
"@types/node": "14.x",
"@types/vscode-webview": "^1.57.0",
"lodash.throttle": "^4.1.1"
},
"repository": {

View File

@@ -31,4 +31,4 @@ export class ActiveLineMarker {
}
element.className += ' code-active-line';
}
}
}

View File

@@ -9,4 +9,4 @@ export function onceDocumentLoaded(f: () => void) {
} else {
f();
}
}
}

View File

@@ -10,8 +10,6 @@ import { getEditorLineNumberForPageOffset, scrollToRevealSourceLine, getLineElem
import { getSettings, getData } from './settings';
import throttle = require('lodash.throttle');
declare let acquireVsCodeApi: any;
let scrollDisabledCount = 0;
const marker = new ActiveLineMarker();
const settings = getSettings();

View File

@@ -41,4 +41,4 @@ export class StyleLoadingMonitor {
poster.postMessage('previewStyleLoadError', { unloadedStyles: this.unloadedStyles });
}
}
}
}

View File

@@ -14,4 +14,4 @@ declare global {
}
window.cspAlerter = new CspAlerter();
window.styleLoadingMonitor = new StyleLoadingMonitor();
window.styleLoadingMonitor = new StyleLoadingMonitor();

View File

@@ -8,5 +8,10 @@
"DOM",
"DOM.Iterable"
]
},
"typeAcquisition": {
"include": [
"@types/vscode-webview"
]
}
}

View File

@@ -35,4 +35,4 @@ export class CommandManager {
this.commands.set(id, vscode.commands.registerCommand(id, impl, thisArg));
}
}
}

View File

@@ -3,11 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export { OpenDocumentLinkCommand } from './openDocumentLink';
export { ShowPreviewCommand, ShowPreviewToSideCommand, ShowLockedPreviewToSideCommand } from './showPreview';
export { ShowSourceCommand } from './showSource';
export { RefreshPreviewCommand } from './refreshPreview';
export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
export { MoveCursorToPositionCommand } from './moveCursorToPosition';
export { ToggleLockCommand } from './toggleLock';
export { OpenDocumentLinkCommand } from './openDocumentLink';
export { RefreshPreviewCommand } from './refreshPreview';
export { ReloadPlugins } from './reloadPlugins';
export { RenderDocument } from './renderDocument';
export { ShowLockedPreviewToSideCommand, ShowPreviewCommand, ShowPreviewToSideCommand } from './showPreview';
export { ShowPreviewSecuritySelectorCommand } from './showPreviewSecuritySelector';
export { ShowSourceCommand } from './showSource';
export { ToggleLockCommand } from './toggleLock';

View File

@@ -19,4 +19,4 @@ export class RefreshPreviewCommand implements Command {
this.engine.cleanCache();
this.webviewManager.refresh();
}
}
}

View File

@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Command } from '../commandManager';
import { MarkdownPreviewManager } from '../features/previewManager';
import { MarkdownEngine } from '../markdownEngine';
export class ReloadPlugins implements Command {
public readonly id = 'markdown.api.reloadPlugins';
public constructor(
private readonly webviewManager: MarkdownPreviewManager,
private readonly engine: MarkdownEngine,
) { }
public execute(): void {
this.engine.reloadPlugins();
this.engine.cleanCache();
this.webviewManager.refresh();
}
}

View File

@@ -27,4 +27,4 @@ export class ShowPreviewSecuritySelectorCommand implements Command {
this.previewSecuritySelector.showSecuritySelectorForResource(vscode.window.activeTextEditor.document.uri);
}
}
}
}

View File

@@ -23,4 +23,4 @@ export class ShowSourceCommand implements Command {
}
return undefined;
}
}
}

View File

@@ -16,4 +16,4 @@ export class ToggleLockCommand implements Command {
public execute() {
this.previewManager.toggleLock();
}
}
}

View File

@@ -52,12 +52,7 @@ function registerMarkdownLanguageFeatures(
): vscode.Disposable {
const selector: vscode.DocumentSelector = { language: 'markdown', scheme: '*' };
const charPattern = '(\\p{Alphabetic}|\\p{Number}|\\p{Nonspacing_Mark})';
return vscode.Disposable.from(
vscode.languages.setLanguageConfiguration('markdown', {
wordPattern: new RegExp(`${charPattern}((${charPattern}|[_])?${charPattern})*`, 'ug'),
}),
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()),
vscode.languages.registerFoldingRangeProvider(selector, new MarkdownFoldingProvider(engine)),
@@ -85,5 +80,6 @@ function registerMarkdownCommands(
commandManager.register(new commands.OpenDocumentLinkCommand(engine));
commandManager.register(new commands.ToggleLockCommand(previewManager));
commandManager.register(new commands.RenderDocument(engine));
commandManager.register(new commands.ReloadPlugins(previewManager, engine));
return commandManager;
}

View File

@@ -72,4 +72,4 @@ export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolPr
private getSymbolName(entry: TocEntry): string {
return '#'.repeat(entry.level) + ' ' + entry.text;
}
}
}

View File

@@ -11,8 +11,8 @@ import { Logger } from '../logger';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { normalizeResource, WebviewResourceProvider } from '../util/resources';
import { getVisibleLine, TopmostLineMonitor } from '../util/topmostLineMonitor';
import { WebviewResourceProvider } from '../util/resources';
import { getVisibleLine, LastScrollLocation, TopmostLineMonitor } from '../util/topmostLineMonitor';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider, MarkdownContentProviderOutput } from './previewContentProvider';
import { MarkdownEngine } from '../markdownEngine';
@@ -120,6 +120,8 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private imageInfo: { readonly id: string, readonly width: number, readonly height: number; }[] = [];
private readonly _fileWatchersBySrc = new Map</* src: */ string, vscode.FileSystemWatcher>();
private readonly _onScrollEmitter = this._register(new vscode.EventEmitter<LastScrollLocation>());
public readonly onScroll = this._onScrollEmitter.event;
constructor(
webview: vscode.WebviewPanel,
@@ -324,7 +326,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
private onDidScrollPreview(line: number) {
this.line = line;
this._onScrollEmitter.fire({ line: this.line, uri: this._resource });
const config = this._previewConfigurations.loadAndCacheConfiguration(this._resource);
if (!config.scrollEditorWithPreview) {
return;
@@ -336,13 +338,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
}
this.isScrolling = true;
const sourceLine = Math.floor(line);
const fraction = line - sourceLine;
const text = editor.document.lineAt(sourceLine).text;
const start = Math.floor(fraction * text.length);
editor.revealRange(
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
vscode.TextEditorRevealType.AtTop);
scrollEditorToLine(line, editor);
}
}
@@ -427,12 +423,12 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
baseRoots.push(vscode.Uri.file(path.dirname(this._resource.fsPath)));
}
return baseRoots.map(root => normalizeResource(this._resource, root));
return baseRoots;
}
private async onDidClickPreviewLink(href: string) {
let [hrefPath, fragment] = decodeURIComponent(href).split('#');
let [hrefPath, fragment] = href.split('#').map(c => decodeURIComponent(c));
if (hrefPath[0] !== '/') {
// We perviously already resolve absolute paths.
@@ -460,7 +456,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
//#region WebviewResourceProvider
asWebviewUri(resource: vscode.Uri) {
return this._webviewPanel.webview.asWebviewUri(normalizeResource(this._resource, resource));
return this._webviewPanel.webview.asWebviewUri(resource);
}
get cspSource() {
@@ -497,11 +493,13 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
webview: vscode.WebviewPanel,
contentProvider: MarkdownContentProvider,
previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
scrollLine?: number,
): StaticMarkdownPreview {
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, logger, contributionProvider, engine);
return new StaticMarkdownPreview(webview, resource, contentProvider, previewConfigurations, topmostLineMonitor, logger, contributionProvider, engine, scrollLine);
}
private readonly preview: MarkdownPreview;
@@ -511,13 +509,15 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
resource: vscode.Uri,
contentProvider: MarkdownContentProvider,
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
topmostLineMonitor: TopmostLineMonitor,
logger: Logger,
contributionProvider: MarkdownContributionProvider,
engine: MarkdownEngine,
scrollLine?: number,
) {
super();
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, undefined, {
const topScrollLocation = scrollLine ? new StartingScrollLine(scrollLine) : undefined;
this.preview = this._register(new MarkdownPreview(this._webviewPanel, resource, topScrollLocation, {
getAdditionalState: () => { return {}; },
openPreviewLinkToMarkdownFile: () => { /* todo */ }
}, engine, contentProvider, _previewConfigurations, logger, contributionProvider));
@@ -529,6 +529,16 @@ export class StaticMarkdownPreview extends Disposable implements ManagedMarkdown
this._register(this._webviewPanel.onDidChangeViewState(e => {
this._onDidChangeViewState.fire(e);
}));
this._register(this.preview.onScroll((scrollInfo) => {
topmostLineMonitor.setPreviousEditorLine(scrollInfo);
}));
this._register(topmostLineMonitor.onDidChanged(event => {
if (this.preview.isPreviewOf(event.resource)) {
this.preview.scrollTo(event.line);
}
}));
}
private readonly _onDispose = this._register(new vscode.EventEmitter<void>());
@@ -789,3 +799,18 @@ export class DynamicMarkdownPreview extends Disposable implements ManagedMarkdow
}
}
/**
* Change the top-most visible line of `editor` to be at `line`
*/
export function scrollEditorToLine(
line: number,
editor: vscode.TextEditor
) {
const sourceLine = Math.floor(line);
const fraction = line - sourceLine;
const text = editor.document.lineAt(sourceLine).text;
const start = Math.floor(fraction * text.length);
editor.revealRange(
new vscode.Range(sourceLine, start, sourceLine + 1, 0),
vscode.TextEditorRevealType.AtTop);
}

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { equals } from '../util/arrays';
export class MarkdownPreviewConfiguration {
public static getForResource(resource: vscode.Uri) {
@@ -21,7 +22,7 @@ export class MarkdownPreviewConfiguration {
public readonly lineHeight: number;
public readonly fontSize: number;
public readonly fontFamily: string | undefined;
public readonly styles: string[];
public readonly styles: readonly string[];
private constructor(resource: vscode.Uri) {
const editorConfig = vscode.workspace.getConfiguration('editor', resource);
@@ -49,7 +50,7 @@ export class MarkdownPreviewConfiguration {
}
public isEqualTo(otherConfig: MarkdownPreviewConfiguration) {
for (let key in this) {
for (const key in this) {
if (this.hasOwnProperty(key) && key !== 'styles') {
if (this[key] !== otherConfig[key]) {
return false;
@@ -57,17 +58,7 @@ export class MarkdownPreviewConfiguration {
}
}
// Check styles
if (this.styles.length !== otherConfig.styles.length) {
return false;
}
for (let i = 0; i < this.styles.length; ++i) {
if (this.styles[i] !== otherConfig.styles[i]) {
return false;
}
}
return true;
return equals(this.styles, otherConfig.styles);
}
[key: string]: any;

View File

@@ -81,7 +81,7 @@ export class MarkdownContentProvider {
const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
const csp = this.getCsp(resourceProvider, sourceUri, nonce);
const body = await this.engine.render(markdownDocument);
const body = await this.engine.render(markdownDocument, resourceProvider);
const html = `<!DOCTYPE html>
<html style="${escapeAttribute(this.getSettingsOverrideStyles(config))}">
<head>

View File

@@ -9,9 +9,10 @@ import { MarkdownEngine } from '../markdownEngine';
import { MarkdownContributionProvider } from '../markdownExtensions';
import { Disposable, disposeAll } from '../util/dispose';
import { TopmostLineMonitor } from '../util/topmostLineMonitor';
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview } from './preview';
import { DynamicMarkdownPreview, ManagedMarkdownPreview, StartingScrollFragment, StaticMarkdownPreview, scrollEditorToLine } from './preview';
import { MarkdownPreviewConfigurationManager } from './previewConfig';
import { MarkdownContentProvider } from './previewContentProvider';
import { isMarkdownFile } from '../util/file';
export interface DynamicPreviewSettings {
readonly resourceColumn: vscode.ViewColumn;
@@ -75,6 +76,17 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
super();
this._register(vscode.window.registerWebviewPanelSerializer(DynamicMarkdownPreview.viewType, this));
this._register(vscode.window.registerCustomEditorProvider(this.customEditorViewType, this));
this._register(vscode.window.onDidChangeActiveTextEditor(textEditor => {
// When at a markdown file, apply existing scroll settings
if (textEditor && textEditor.document && isMarkdownFile(textEditor.document)) {
const line = this._topmostLineMonitor.getPreviousEditorLineByUri(textEditor.document.uri);
if (line) {
scrollEditorToLine(line, textEditor);
}
}
}));
}
public refresh() {
@@ -160,14 +172,18 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
document: vscode.TextDocument,
webview: vscode.WebviewPanel
): Promise<void> {
const lineNumber = this._topmostLineMonitor.getPreviousEditorLineByUri(document.uri);
const preview = StaticMarkdownPreview.revive(
document.uri,
webview,
this._contentProvider,
this._previewConfigurations,
this._topmostLineMonitor,
this._logger,
this._contributions,
this._engine);
this._engine,
lineNumber
);
this.registerStaticPreview(preview);
}
@@ -175,11 +191,14 @@ export class MarkdownPreviewManager extends Disposable implements vscode.Webview
resource: vscode.Uri,
previewSettings: DynamicPreviewSettings
): DynamicMarkdownPreview {
const activeTextEditorURI = vscode.window.activeTextEditor?.document.uri;
const scrollLine = (activeTextEditorURI?.toString() === resource.toString()) ? vscode.window.activeTextEditor?.visibleRanges[0].start.line : undefined;
const preview = DynamicMarkdownPreview.create(
{
resource,
resourceColumn: previewSettings.resourceColumn,
locked: previewSettings.locked,
line: scrollLine,
},
previewSettings.previewColumn,
this._contentProvider,

View File

@@ -4,13 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { MarkdownIt, Token } from 'markdown-it';
import * as path from 'path';
import * as vscode from 'vscode';
import { MarkdownContributionProvider as MarkdownContributionProvider } from './markdownExtensions';
import { Slugifier } from './slugify';
import { SkinnyTextDocument } from './tableOfContentsProvider';
import { hash } from './util/hash';
import { isOfScheme, MarkdownFileExtensions, Schemes } from './util/links';
import { isOfScheme, Schemes } from './util/links';
import { WebviewResourceProvider } from './util/resources';
const UNICODE_NEWLINE_REGEX = /\u2028|\u2029/g;
@@ -62,12 +62,14 @@ export interface RenderOutput {
interface RenderEnv {
containingImages: { src: string }[];
currentDocument: vscode.Uri | undefined;
resourceProvider: WebviewResourceProvider | undefined;
}
export class MarkdownEngine {
private md?: Promise<MarkdownIt>;
private currentDocument?: vscode.Uri;
private _slugCount = new Map<string, number>();
private _tokenCache = new TokenCache();
@@ -113,7 +115,7 @@ export class MarkdownEngine {
this.addLineNumberRenderer(md, renderName);
}
this.addImageStabilizer(md);
this.addImageRenderer(md);
this.addFencedRenderer(md);
this.addLinkNormalizer(md);
this.addLinkValidator(md);
@@ -128,6 +130,10 @@ export class MarkdownEngine {
return md;
}
public reloadPlugins() {
this.md = undefined;
}
private tokenizeDocument(
document: SkinnyTextDocument,
config: MarkdownItConfig,
@@ -138,27 +144,18 @@ export class MarkdownEngine {
return cached;
}
this.currentDocument = document.uri;
const tokens = this.tokenizeString(document.getText(), engine);
this._tokenCache.update(document, config, tokens);
return tokens;
}
public async renderText(document: vscode.Uri, text: string): Promise<string> { // {{SQL CARBON EDIT}} - Add renderText method
const config = this.getConfig(document);
const engine = await this.getEngine(config);
this.currentDocument = document;
return engine.render(text, config);
}
private tokenizeString(text: string, engine: MarkdownIt) {
this._slugCount = new Map<string, number>();
return engine.parse(text.replace(UNICODE_NEWLINE_REGEX, ''), {});
}
public async render(input: SkinnyTextDocument | string): Promise<RenderOutput> {
public async render(input: SkinnyTextDocument | string, resourceProvider?: WebviewResourceProvider): Promise<RenderOutput> {
const config = this.getConfig(typeof input === 'string' ? undefined : input.uri);
const engine = await this.getEngine(config);
@@ -167,7 +164,9 @@ export class MarkdownEngine {
: this.tokenizeDocument(input, config, engine);
const env: RenderEnv = {
containingImages: []
containingImages: [],
currentDocument: typeof input === 'string' ? undefined : input.uri,
resourceProvider,
};
const html = engine.renderer.render(tokens, {
@@ -200,12 +199,12 @@ export class MarkdownEngine {
};
}
private addLineNumberRenderer(md: any, ruleName: string): void {
private addLineNumberRenderer(md: MarkdownIt, ruleName: string): void {
const original = md.renderer.rules[ruleName];
md.renderer.rules[ruleName] = (tokens: any, idx: number, options: any, env: any, self: any) => {
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.attrSet('data-line', token.map[0] + '');
token.attrJoin('class', 'code-line');
}
@@ -217,9 +216,9 @@ export class MarkdownEngine {
};
}
private addImageStabilizer(md: any): void {
private addImageRenderer(md: MarkdownIt): void {
const original = md.renderer.rules.image;
md.renderer.rules.image = (tokens: any, idx: number, options: any, env: RenderEnv, self: any) => {
md.renderer.rules.image = (tokens: Token[], idx: number, options: any, env: RenderEnv, self: any) => {
const token = tokens[idx];
token.attrJoin('class', 'loading');
@@ -228,6 +227,11 @@ export class MarkdownEngine {
env.containingImages?.push({ src });
const imgHash = hash(src);
token.attrSet('id', `image-hash-${imgHash}`);
if (!token.attrGet('data-src')) {
token.attrSet('src', this.toResourceUri(src, env.currentDocument, env.resourceProvider));
token.attrSet('data-src', src);
}
}
if (original) {
@@ -238,9 +242,9 @@ export class MarkdownEngine {
};
}
private addFencedRenderer(md: any): void {
private addFencedRenderer(md: MarkdownIt): void {
const original = md.renderer.rules['fenced'];
md.renderer.rules['fenced'] = (tokens: any, idx: number, options: any, env: any, self: any) => {
md.renderer.rules['fenced'] = (tokens: Token[], idx: number, options: any, env: any, self: any) => {
const token = tokens[idx];
if (token.map && token.map.length) {
token.attrJoin('class', 'hljs');
@@ -250,7 +254,7 @@ export class MarkdownEngine {
};
}
private addLinkNormalizer(md: any): void {
private addLinkNormalizer(md: MarkdownIt): void {
const normalizeLink = md.normalizeLink;
md.normalizeLink = (link: string) => {
try {
@@ -259,43 +263,6 @@ export class MarkdownEngine {
return normalizeLink(vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }).toString());
}
// Support file:// links
if (isOfScheme(Schemes.file, link)) {
// Ensure link is relative by prepending `/` so that it uses the <base> element URI
// when resolving the absolute URL
return normalizeLink('/' + link.replace(/^file:/, 'file'));
}
// If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace
if (!/^[a-z\-]+:/i.test(link)) {
// Use a fake scheme for parsing
let uri = vscode.Uri.parse('markdown-link:' + link);
// Relative paths should be resolved correctly inside the preview but we need to
// handle absolute paths specially (for images) to resolve them relative to the workspace root
if (uri.path[0] === '/') {
const root = vscode.workspace.getWorkspaceFolder(this.currentDocument!);
if (root) {
const fileUri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({
fragment: uri.fragment,
query: uri.query,
});
// Ensure fileUri is relative by prepending `/` so that it uses the <base> element URI
// when resolving the absolute URL
uri = vscode.Uri.parse('markdown-link:' + '/' + fileUri.toString(true).replace(/^\S+?:/, fileUri.scheme));
}
}
const extname = path.extname(uri.fsPath);
if (uri.fragment && (extname === '' || MarkdownFileExtensions.includes(extname))) {
uri = uri.with({
fragment: this.slugifier.fromHeading(uri.fragment).value
});
}
return normalizeLink(uri.toString(true).replace(/^markdown-link:/, ''));
}
} catch (e) {
// noop
}
@@ -303,7 +270,7 @@ export class MarkdownEngine {
};
}
private addLinkValidator(md: any): void {
private addLinkValidator(md: MarkdownIt): void {
const validateLink = md.validateLink;
md.validateLink = (link: string) => {
return validateLink(link)
@@ -313,9 +280,9 @@ export class MarkdownEngine {
};
}
private addNamedHeaders(md: any): void {
private addNamedHeaders(md: MarkdownIt): void {
const original = md.renderer.rules.heading_open;
md.renderer.rules.heading_open = (tokens: any, idx: number, options: any, env: any, self: any) => {
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, '');
let slug = this.slugifier.fromHeading(title);
@@ -338,12 +305,12 @@ export class MarkdownEngine {
};
}
private addLinkRenderer(md: any): void {
const old_render = md.renderer.rules.link_open || ((tokens: any, idx: number, options: any, _env: any, self: any) => {
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);
});
md.renderer.rules.link_open = (tokens: any, idx: number, options: any, env: any, self: any) => {
md.renderer.rules.link_open = (tokens: Token[], idx: number, options: any, env: any, self: any) => {
const token = tokens[idx];
const hrefIndex = token.attrIndex('href');
if (hrefIndex >= 0) {
@@ -353,6 +320,50 @@ export class MarkdownEngine {
return old_render(tokens, idx, options, env, self);
};
}
private toResourceUri(href: string, currentDocument: vscode.Uri | undefined, resourceProvider: WebviewResourceProvider | undefined): string {
try {
// Support file:// links
if (isOfScheme(Schemes.file, href)) {
const uri = vscode.Uri.parse(href);
if (resourceProvider) {
return resourceProvider.asWebviewUri(uri).toString(true);
}
// Not sure how to resolve this
return href;
}
// If original link doesn't look like a url with a scheme, assume it must be a link to a file in workspace
if (!/^[a-z\-]+:/i.test(href)) {
// Use a fake scheme for parsing
let uri = vscode.Uri.parse('markdown-link:' + href);
// Relative paths should be resolved correctly inside the preview but we need to
// handle absolute paths specially to resolve them relative to the workspace root
if (uri.path[0] === '/' && currentDocument) {
const root = vscode.workspace.getWorkspaceFolder(currentDocument);
if (root) {
uri = vscode.Uri.joinPath(root.uri, uri.fsPath).with({
fragment: uri.fragment,
query: uri.query,
});
if (resourceProvider) {
return resourceProvider.asWebviewUri(uri).toString(true);
} else {
uri = uri.with({ scheme: 'markdown-link' });
}
}
}
return uri.toString(true).replace(/^markdown-link:/, '');
}
return href;
} catch {
return href;
}
}
}
async function getMarkdownOptions(md: () => MarkdownIt) {

View File

@@ -37,11 +37,11 @@ suite('markdown.engine', () => {
const engine = createNewMarkdownEngine();
assert.deepStrictEqual((await engine.render(input)), {
html: '<p data-line="0" class="code-line">'
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435"> '
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435" data-src="img.png"> '
+ '<a href="no-img.png" data-href="no-img.png"></a> '
+ '<img src="http://example.org/img.png" alt="" class="loading" id="image-hash--1903814170"> '
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435"> '
+ '<img src="./img2.png" alt="" class="loading" id="image-hash-265238964">'
+ '<img src="http://example.org/img.png" alt="" class="loading" id="image-hash--1903814170" data-src="http://example.org/img.png"> '
+ '<img src="img.png" alt="" class="loading" id="image-hash--754511435" data-src="img.png"> '
+ '<img src="./img2.png" alt="" class="loading" id="image-hash-265238964" data-src="./img2.png">'
+ '</p>\n'
,
containingImages: [{ src: 'img.png' }, { src: 'http://example.org/img.png' }, { src: 'img.png' }, { src: './img2.png' }],

View File

@@ -18,7 +18,7 @@ suite('markdown.WorkspaceSymbolProvider', () => {
test('Should not return anything for empty workspace', async () => {
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([]));
assert.deepEqual(await provider.provideWorkspaceSymbols(''), []);
assert.deepStrictEqual(await provider.provideWorkspaceSymbols(''), []);
});
test('Should return symbols from workspace with one markdown file', async () => {

View File

@@ -15,4 +15,4 @@ export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEq
}
return true;
}
}

View File

@@ -39,4 +39,4 @@ export abstract class Disposable {
protected get isDisposed() {
return this._isDisposed;
}
}
}

View File

@@ -7,4 +7,4 @@ import * as vscode from 'vscode';
export function isMarkdownFile(document: vscode.TextDocument) {
return document.languageId === 'markdown';
}
}

View File

@@ -36,4 +36,4 @@ class LazyValue<T> implements Lazy<T> {
export function lazy<T>(getValue: () => T): Lazy<T> {
return new LazyValue<T>(getValue);
}
}

View File

@@ -14,7 +14,6 @@ export const Schemes = {
data: 'data:',
vscode: 'vscode:',
'vscode-insiders': 'vscode-insiders:',
'vscode-resource': 'vscode-resource:',
};
const knownSchemes = [

View File

@@ -11,23 +11,3 @@ export interface WebviewResourceProvider {
readonly cspSource: string;
}
export function normalizeResource(
base: vscode.Uri,
resource: vscode.Uri
): vscode.Uri {
// If we have a windows path and are loading a workspace with an authority,
// make sure we use a unc path with an explicit localhost authority.
//
// Otherwise, the `<base>` rule will insert the authority into the resolved resource
// URI incorrectly.
if (base.authority && !resource.authority) {
const driveMatch = resource.path.match(/^\/(\w):\//);
if (driveMatch) {
return vscode.Uri.file(`\\\\localhost\\${driveMatch[1]}$\\${resource.fsPath.replace(/^\w:\\/, '')}`).with({
fragment: resource.fragment,
query: resource.query
});
}
}
return resource;
}

View File

@@ -7,18 +7,32 @@ import * as vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from './file';
export interface LastScrollLocation {
readonly line: number;
readonly uri: vscode.Uri;
}
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;
constructor() {
super();
if (vscode.window.activeTextEditor) {
const line = getVisibleLine(vscode.window.activeTextEditor);
this.setPreviousEditorLine({ uri: vscode.window.activeTextEditor.document.uri, line: line ?? 0 });
}
this._register(vscode.window.onDidChangeTextEditorVisibleRanges(event => {
if (isMarkdownFile(event.textEditor.document)) {
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 });
}
}
}));
@@ -27,7 +41,16 @@ 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;
private updateLine(
public setPreviousEditorLine(scrollLocation: LastScrollLocation): void {
this.previousEditorInfo.set(scrollLocation.uri.toString(), scrollLocation);
}
public getPreviousEditorLineByUri(resource: vscode.Uri): number | undefined {
const scrollLoc = this.previousEditorInfo.get(resource.toString());
return scrollLoc?.line;
}
public updateLine(
resource: vscode.Uri,
line: number
) {

View File

@@ -26,10 +26,15 @@
resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.2.tgz#5d9ad19e6e6508cdd2f2596df86fd0aade598660"
integrity sha1-XZrRnm5lCM3S8llt+G/Qqt5ZhmA=
"@types/node@^12.19.9":
version "12.20.6"
resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.6.tgz#7b73cce37352936e628c5ba40326193443cfba25"
integrity sha512-sRVq8d+ApGslmkE9e3i+D3gFGk7aZHAT+G4cIpIEdLJYPsWiSPwcAnJEjddLQQDqV3Ra2jOclX/Sv6YrvGYiWA==
"@types/node@14.x":
version "14.14.43"
resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.43.tgz#26bcbb0595b305400e8ceaf9a127a7f905ae49c8"
integrity sha512-3pwDJjp1PWacPTpH0LcfhgjvurQvrZFBrC6xxjaUEZ7ifUtT32jtjPxEMMblpqd2Mvx+k8haqQJLQxolyGN/cQ==
"@types/vscode-webview@^1.57.0":
version "1.57.0"
resolved "https://registry.yarnpkg.com/@types/vscode-webview/-/vscode-webview-1.57.0.tgz#bad5194d45ae8d03afc1c0f67f71ff5e7a243bbf"
integrity sha512-x3Cb/SMa1IwRHfSvKaZDZOTh4cNoG505c3NjTqGlMC082m++x/ETUmtYniDsw6SSmYzZXO8KBNhYxR0+VqymqA==
applicationinsights@1.7.4:
version "1.7.4"