mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 18:46:40 -05:00
add markdown-language-features to sqlops (#2338)
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
|
||||
|
||||
function normalizeLink(
|
||||
document: vscode.TextDocument,
|
||||
link: string,
|
||||
base: string
|
||||
): vscode.Uri {
|
||||
const uri = vscode.Uri.parse(link);
|
||||
if (uri.scheme) {
|
||||
return uri;
|
||||
}
|
||||
|
||||
// assume it must be a file
|
||||
let resourcePath = uri.path;
|
||||
if (!uri.path) {
|
||||
resourcePath = document.uri.path;
|
||||
} else if (uri.path[0] === '/') {
|
||||
const root = vscode.workspace.getWorkspaceFolder(document.uri);
|
||||
if (root) {
|
||||
resourcePath = path.join(root.uri.fsPath, uri.path);
|
||||
}
|
||||
} else {
|
||||
resourcePath = path.join(base, uri.path);
|
||||
}
|
||||
|
||||
return OpenDocumentLinkCommand.createCommandUri(resourcePath, uri.fragment);
|
||||
}
|
||||
|
||||
function matchAll(
|
||||
pattern: RegExp,
|
||||
text: string
|
||||
): Array<RegExpMatchArray> {
|
||||
const out: RegExpMatchArray[] = [];
|
||||
pattern.lastIndex = 0;
|
||||
let match: RegExpMatchArray | null;
|
||||
while ((match = pattern.exec(text))) {
|
||||
out.push(match);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
private readonly linkPattern = /(\[[^\]]*\]\(\s*?)(((((?=.*\)\)+)|(?=.*\)\]+))[^\s\)]+?)|([^\s]+?)))\)/g;
|
||||
private readonly referenceLinkPattern = /(\[([^\]]+)\]\[\s*?)([^\s\]]*?)\]/g;
|
||||
private readonly definitionPattern = /^([\t ]*\[([^\]]+)\]:\s*)(\S+)/gm;
|
||||
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument,
|
||||
_token: vscode.CancellationToken
|
||||
): vscode.DocumentLink[] {
|
||||
const base = path.dirname(document.uri.fsPath);
|
||||
const text = document.getText();
|
||||
|
||||
return this.providerInlineLinks(text, document, base)
|
||||
.concat(this.provideReferenceLinks(text, document, base));
|
||||
}
|
||||
|
||||
private providerInlineLinks(
|
||||
text: string,
|
||||
document: vscode.TextDocument,
|
||||
base: string
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
for (const match of matchAll(this.linkPattern, text)) {
|
||||
const pre = match[1];
|
||||
const link = match[2];
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
try {
|
||||
results.push(new vscode.DocumentLink(
|
||||
new vscode.Range(linkStart, linkEnd),
|
||||
normalizeLink(document, link, base)));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private provideReferenceLinks(
|
||||
text: string,
|
||||
document: vscode.TextDocument,
|
||||
base: string
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
|
||||
const definitions = this.getDefinitions(text, document);
|
||||
for (const match of matchAll(this.referenceLinkPattern, text)) {
|
||||
let linkStart: vscode.Position;
|
||||
let linkEnd: vscode.Position;
|
||||
let reference = match[3];
|
||||
if (reference) { // [text][ref]
|
||||
const pre = match[1];
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + reference.length);
|
||||
} else if (match[2]) { // [ref][]
|
||||
reference = match[2];
|
||||
const offset = (match.index || 0) + 1;
|
||||
linkStart = document.positionAt(offset);
|
||||
linkEnd = document.positionAt(offset + match[2].length);
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const link = definitions.get(reference);
|
||||
if (link) {
|
||||
results.push(new vscode.DocumentLink(
|
||||
new vscode.Range(linkStart, linkEnd),
|
||||
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([link.linkRange.start.line, link.linkRange.start.character]))}`)));
|
||||
}
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
for (const definition of Array.from(definitions.values())) {
|
||||
try {
|
||||
results.push(new vscode.DocumentLink(
|
||||
definition.linkRange,
|
||||
normalizeLink(document, definition.link, base)));
|
||||
} catch (e) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private getDefinitions(text: string, document: vscode.TextDocument) {
|
||||
const out = new Map<string, { link: string, linkRange: vscode.Range }>();
|
||||
for (const match of matchAll(this.definitionPattern, text)) {
|
||||
const pre = match[1];
|
||||
const reference = match[2];
|
||||
const link = match[3].trim();
|
||||
|
||||
const offset = (match.index || 0) + pre.length;
|
||||
const linkStart = document.positionAt(offset);
|
||||
const linkEnd = document.positionAt(offset + link.length);
|
||||
|
||||
out.set(reference, {
|
||||
link: link,
|
||||
linkRange: new vscode.Range(linkStart, linkEnd)
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
|
||||
|
||||
export default class MDDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideDocumentSymbols(document: vscode.TextDocument): Promise<vscode.SymbolInformation[]> {
|
||||
const toc = await new TableOfContentsProvider(this.engine, document).getToc();
|
||||
return toc.map(entry => {
|
||||
return new vscode.SymbolInformation('#'.repeat(entry.level) + ' ' + entry.text, vscode.SymbolKind.String, '', entry.location);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
|
||||
const rangeLimit = 5000;
|
||||
|
||||
export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
389
extensions/markdown-language-features/src/features/preview.ts
Normal file
389
extensions/markdown-language-features/src/features/preview.ts
Normal file
@@ -0,0 +1,389 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
import { disposeAll } from '../util/dispose';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
import { getVisibleLine, MarkdownFileTopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContributions } from '../markdownExtensions';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
export class MarkdownPreview {
|
||||
|
||||
public static viewType = 'markdown.preview';
|
||||
|
||||
private _resource: vscode.Uri;
|
||||
private _locked: boolean;
|
||||
|
||||
private readonly editor: vscode.WebviewPanel;
|
||||
private throttleTimer: any;
|
||||
private line: number | undefined = undefined;
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
private firstUpdate = true;
|
||||
private currentVersion?: { resource: vscode.Uri, version: number };
|
||||
private forceUpdate = false;
|
||||
private isScrolling = false;
|
||||
private _disposed: boolean = false;
|
||||
|
||||
|
||||
public static async revive(
|
||||
webview: vscode.WebviewPanel,
|
||||
state: any,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
topmostLineMonitor: MarkdownFileTopmostLineMonitor
|
||||
): Promise<MarkdownPreview> {
|
||||
const resource = vscode.Uri.parse(state.resource);
|
||||
const locked = state.locked;
|
||||
const line = state.line;
|
||||
|
||||
const preview = new MarkdownPreview(
|
||||
webview,
|
||||
resource,
|
||||
locked,
|
||||
contentProvider,
|
||||
previewConfigurations,
|
||||
logger,
|
||||
topmostLineMonitor);
|
||||
|
||||
if (!isNaN(line)) {
|
||||
preview.line = line;
|
||||
}
|
||||
await preview.doUpdate();
|
||||
return preview;
|
||||
}
|
||||
|
||||
public static create(
|
||||
resource: vscode.Uri,
|
||||
previewColumn: vscode.ViewColumn,
|
||||
locked: boolean,
|
||||
contentProvider: MarkdownContentProvider,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
logger: Logger,
|
||||
topmostLineMonitor: MarkdownFileTopmostLineMonitor,
|
||||
contributions: MarkdownContributions
|
||||
): MarkdownPreview {
|
||||
const webview = vscode.window.createWebviewPanel(
|
||||
MarkdownPreview.viewType,
|
||||
MarkdownPreview.getPreviewTitle(resource, locked),
|
||||
previewColumn, {
|
||||
enableScripts: true,
|
||||
enableCommandUris: true,
|
||||
enableFindWidget: true,
|
||||
localResourceRoots: MarkdownPreview.getLocalResourceRoots(resource, contributions)
|
||||
});
|
||||
|
||||
return new MarkdownPreview(
|
||||
webview,
|
||||
resource,
|
||||
locked,
|
||||
contentProvider,
|
||||
previewConfigurations,
|
||||
logger,
|
||||
topmostLineMonitor);
|
||||
}
|
||||
|
||||
private constructor(
|
||||
webview: vscode.WebviewPanel,
|
||||
resource: vscode.Uri,
|
||||
locked: boolean,
|
||||
private readonly _contentProvider: MarkdownContentProvider,
|
||||
private readonly _previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
private readonly _logger: Logger,
|
||||
topmostLineMonitor: MarkdownFileTopmostLineMonitor
|
||||
) {
|
||||
this._resource = resource;
|
||||
this._locked = locked;
|
||||
this.editor = webview;
|
||||
|
||||
this.editor.onDidDispose(() => {
|
||||
this.dispose();
|
||||
}, null, this.disposables);
|
||||
|
||||
this.editor.onDidChangeViewState(e => {
|
||||
this._onDidChangeViewStateEmitter.fire(e);
|
||||
}, null, this.disposables);
|
||||
|
||||
this.editor.webview.onDidReceiveMessage(e => {
|
||||
if (e.source !== this._resource.toString()) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch (e.type) {
|
||||
case 'command':
|
||||
vscode.commands.executeCommand(e.body.command, ...e.body.args);
|
||||
break;
|
||||
|
||||
case 'revealLine':
|
||||
this.onDidScrollPreview(e.body.line);
|
||||
break;
|
||||
|
||||
case 'didClick':
|
||||
this.onDidClickPreview(e.body.line);
|
||||
break;
|
||||
|
||||
}
|
||||
}, null, this.disposables);
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument(event => {
|
||||
if (this.isPreviewOf(event.document.uri)) {
|
||||
this.refresh();
|
||||
}
|
||||
}, null, this.disposables);
|
||||
|
||||
topmostLineMonitor.onDidChangeTopmostLine(event => {
|
||||
if (this.isPreviewOf(event.resource)) {
|
||||
this.updateForView(event.resource, event.line);
|
||||
}
|
||||
}, null, this.disposables);
|
||||
|
||||
vscode.window.onDidChangeTextEditorSelection(event => {
|
||||
if (this.isPreviewOf(event.textEditor.document.uri)) {
|
||||
this.postMessage({
|
||||
type: 'onDidChangeTextEditorSelection',
|
||||
line: event.selections[0].active.line,
|
||||
source: this.resource.toString()
|
||||
});
|
||||
}
|
||||
}, null, this.disposables);
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
if (editor && isMarkdownFile(editor.document) && !this._locked) {
|
||||
this.update(editor.document.uri);
|
||||
}
|
||||
}, null, this.disposables);
|
||||
}
|
||||
|
||||
private readonly _onDisposeEmitter = new vscode.EventEmitter<void>();
|
||||
public readonly onDispose = this._onDisposeEmitter.event;
|
||||
|
||||
private readonly _onDidChangeViewStateEmitter = new vscode.EventEmitter<vscode.WebviewPanelOnDidChangeViewStateEvent>();
|
||||
public readonly onDidChangeViewState = this._onDidChangeViewStateEmitter.event;
|
||||
|
||||
public get resource(): vscode.Uri {
|
||||
return this._resource;
|
||||
}
|
||||
|
||||
public get state() {
|
||||
return {
|
||||
resource: this.resource.toString(),
|
||||
locked: this._locked,
|
||||
line: this.line
|
||||
};
|
||||
}
|
||||
|
||||
public dispose() {
|
||||
if (this._disposed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._disposed = true;
|
||||
this._onDisposeEmitter.fire();
|
||||
|
||||
this._onDisposeEmitter.dispose();
|
||||
this._onDidChangeViewStateEmitter.dispose();
|
||||
this.editor.dispose();
|
||||
|
||||
disposeAll(this.disposables);
|
||||
}
|
||||
|
||||
public update(resource: vscode.Uri) {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (editor && editor.document.uri.fsPath === resource.fsPath) {
|
||||
this.line = getVisibleLine(editor);
|
||||
}
|
||||
|
||||
// If we have changed resources, cancel any pending updates
|
||||
const isResourceChange = resource.fsPath !== this._resource.fsPath;
|
||||
if (isResourceChange) {
|
||||
clearTimeout(this.throttleTimer);
|
||||
this.throttleTimer = undefined;
|
||||
}
|
||||
|
||||
this._resource = resource;
|
||||
|
||||
// Schedule update if none is pending
|
||||
if (!this.throttleTimer) {
|
||||
if (isResourceChange || this.firstUpdate) {
|
||||
this.doUpdate();
|
||||
} else {
|
||||
this.throttleTimer = setTimeout(() => this.doUpdate(), 300);
|
||||
}
|
||||
}
|
||||
|
||||
this.firstUpdate = false;
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
this.forceUpdate = true;
|
||||
this.update(this._resource);
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
if (this._previewConfigurations.hasConfigurationChanged(this._resource)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public get position(): vscode.ViewColumn | undefined {
|
||||
return this.editor.viewColumn;
|
||||
}
|
||||
|
||||
public isWebviewOf(webview: vscode.WebviewPanel): boolean {
|
||||
return this.editor === webview;
|
||||
}
|
||||
|
||||
public matchesResource(
|
||||
otherResource: vscode.Uri,
|
||||
otherPosition: vscode.ViewColumn | undefined,
|
||||
otherLocked: boolean
|
||||
): boolean {
|
||||
if (this.position !== otherPosition) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this._locked) {
|
||||
return otherLocked && this.isPreviewOf(otherResource);
|
||||
} else {
|
||||
return !otherLocked;
|
||||
}
|
||||
}
|
||||
|
||||
public matches(otherPreview: MarkdownPreview): boolean {
|
||||
return this.matchesResource(otherPreview._resource, otherPreview.position, otherPreview._locked);
|
||||
}
|
||||
|
||||
public reveal(viewColumn: vscode.ViewColumn) {
|
||||
this.editor.reveal(viewColumn);
|
||||
}
|
||||
|
||||
public toggleLock() {
|
||||
this._locked = !this._locked;
|
||||
this.editor.title = MarkdownPreview.getPreviewTitle(this._resource, this._locked);
|
||||
}
|
||||
|
||||
private isPreviewOf(resource: vscode.Uri): boolean {
|
||||
return this._resource.fsPath === resource.fsPath;
|
||||
}
|
||||
|
||||
private static getPreviewTitle(resource: vscode.Uri, locked: boolean): string {
|
||||
return locked
|
||||
? localize('lockedPreviewTitle', '[Preview] {0}', path.basename(resource.fsPath))
|
||||
: localize('previewTitle', 'Preview {0}', path.basename(resource.fsPath));
|
||||
}
|
||||
|
||||
private updateForView(resource: vscode.Uri, topLine: number | undefined) {
|
||||
if (!this.isPreviewOf(resource)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isScrolling) {
|
||||
this.isScrolling = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof topLine === 'number') {
|
||||
this._logger.log('updateForView', { markdownFile: resource });
|
||||
this.line = topLine;
|
||||
this.postMessage({
|
||||
type: 'updateView',
|
||||
line: topLine,
|
||||
source: resource.toString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private postMessage(msg: any) {
|
||||
if (!this._disposed) {
|
||||
this.editor.webview.postMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
private async doUpdate(): Promise<void> {
|
||||
const resource = this._resource;
|
||||
|
||||
clearTimeout(this.throttleTimer);
|
||||
this.throttleTimer = undefined;
|
||||
|
||||
const document = await vscode.workspace.openTextDocument(resource);
|
||||
if (!this.forceUpdate && this.currentVersion && this.currentVersion.resource.fsPath === resource.fsPath && this.currentVersion.version === document.version) {
|
||||
if (this.line) {
|
||||
this.updateForView(resource, this.line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.forceUpdate = false;
|
||||
|
||||
this.currentVersion = { resource, version: document.version };
|
||||
const content = await this._contentProvider.provideTextDocumentContent(document, this._previewConfigurations, this.line);
|
||||
if (this._resource === resource) {
|
||||
this.editor.title = MarkdownPreview.getPreviewTitle(this._resource, this._locked);
|
||||
this.editor.webview.html = content;
|
||||
}
|
||||
}
|
||||
|
||||
private static getLocalResourceRoots(
|
||||
resource: vscode.Uri,
|
||||
contributions: MarkdownContributions
|
||||
): vscode.Uri[] {
|
||||
const baseRoots = contributions.previewResourceRoots;
|
||||
|
||||
const folder = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (folder) {
|
||||
return baseRoots.concat(folder.uri);
|
||||
}
|
||||
|
||||
if (!resource.scheme || resource.scheme === 'file') {
|
||||
return baseRoots.concat(vscode.Uri.file(path.dirname(resource.fsPath)));
|
||||
}
|
||||
|
||||
return baseRoots;
|
||||
}
|
||||
|
||||
private onDidScrollPreview(line: number) {
|
||||
this.line = line;
|
||||
for (const editor of vscode.window.visibleTextEditors) {
|
||||
if (!this.isPreviewOf(editor.document.uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
private async onDidClickPreview(line: number): Promise<void> {
|
||||
for (const visibleEditor of vscode.window.visibleTextEditors) {
|
||||
if (this.isPreviewOf(visibleEditor.document.uri)) {
|
||||
const editor = await vscode.window.showTextDocument(visibleEditor.document, visibleEditor.viewColumn);
|
||||
const position = new vscode.Position(line, 0);
|
||||
editor.selection = new vscode.Selection(position, position);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
vscode.workspace.openTextDocument(this._resource).then(vscode.window.showTextDocument);
|
||||
}
|
||||
}
|
||||
|
||||
export interface PreviewSettings {
|
||||
readonly resourceColumn: vscode.ViewColumn;
|
||||
readonly previewColumn: vscode.ViewColumn;
|
||||
readonly locked: boolean;
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class MarkdownPreviewConfiguration {
|
||||
public static getForResource(resource: vscode.Uri) {
|
||||
return new MarkdownPreviewConfiguration(resource);
|
||||
}
|
||||
|
||||
public readonly scrollBeyondLastLine: boolean;
|
||||
public readonly wordWrap: boolean;
|
||||
public readonly previewFrontMatter: string;
|
||||
public readonly lineBreaks: boolean;
|
||||
public readonly doubleClickToSwitchToEditor: boolean;
|
||||
public readonly scrollEditorWithPreview: boolean;
|
||||
public readonly scrollPreviewWithEditor: boolean;
|
||||
public readonly markEditorSelection: boolean;
|
||||
|
||||
public readonly lineHeight: number;
|
||||
public readonly fontSize: number;
|
||||
public readonly fontFamily: string | undefined;
|
||||
public readonly styles: string[];
|
||||
|
||||
private constructor(resource: vscode.Uri) {
|
||||
const editorConfig = vscode.workspace.getConfiguration('editor', resource);
|
||||
const markdownConfig = vscode.workspace.getConfiguration('markdown', resource);
|
||||
const markdownEditorConfig = vscode.workspace.getConfiguration('[markdown]');
|
||||
|
||||
this.scrollBeyondLastLine = editorConfig.get<boolean>('scrollBeyondLastLine', false);
|
||||
|
||||
this.wordWrap = editorConfig.get<string>('wordWrap', 'off') !== 'off';
|
||||
if (markdownEditorConfig && markdownEditorConfig['editor.wordWrap']) {
|
||||
this.wordWrap = markdownEditorConfig['editor.wordWrap'] !== 'off';
|
||||
}
|
||||
|
||||
this.previewFrontMatter = markdownConfig.get<string>('previewFrontMatter', 'hide');
|
||||
this.scrollPreviewWithEditor = !!markdownConfig.get<boolean>('preview.scrollPreviewWithEditor', true);
|
||||
this.scrollEditorWithPreview = !!markdownConfig.get<boolean>('preview.scrollEditorWithPreview', true);
|
||||
this.lineBreaks = !!markdownConfig.get<boolean>('preview.breaks', false);
|
||||
this.doubleClickToSwitchToEditor = !!markdownConfig.get<boolean>('preview.doubleClickToSwitchToEditor', true);
|
||||
this.markEditorSelection = !!markdownConfig.get<boolean>('preview.markEditorSelection', true);
|
||||
|
||||
this.fontFamily = markdownConfig.get<string | undefined>('preview.fontFamily', undefined);
|
||||
this.fontSize = Math.max(8, +markdownConfig.get<number>('preview.fontSize', NaN));
|
||||
this.lineHeight = Math.max(0.6, +markdownConfig.get<number>('preview.lineHeight', NaN));
|
||||
|
||||
this.styles = markdownConfig.get<string[]>('styles', []);
|
||||
}
|
||||
|
||||
public isEqualTo(otherConfig: MarkdownPreviewConfiguration) {
|
||||
for (let key in this) {
|
||||
if (this.hasOwnProperty(key) && key !== 'styles') {
|
||||
if (this[key] !== otherConfig[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export class MarkdownPreviewConfigurationManager {
|
||||
private readonly previewConfigurationsForWorkspaces = new Map<string, MarkdownPreviewConfiguration>();
|
||||
|
||||
public loadAndCacheConfiguration(
|
||||
resource: vscode.Uri
|
||||
): MarkdownPreviewConfiguration {
|
||||
const config = MarkdownPreviewConfiguration.getForResource(resource);
|
||||
this.previewConfigurationsForWorkspaces.set(this.getKey(resource), config);
|
||||
return config;
|
||||
}
|
||||
|
||||
public hasConfigurationChanged(
|
||||
resource: vscode.Uri
|
||||
): boolean {
|
||||
const key = this.getKey(resource);
|
||||
const currentConfig = this.previewConfigurationsForWorkspaces.get(key);
|
||||
const newConfig = MarkdownPreviewConfiguration.getForResource(resource);
|
||||
return (!currentConfig || !currentConfig.isEqualTo(newConfig));
|
||||
}
|
||||
|
||||
private getKey(
|
||||
resource: vscode.Uri
|
||||
): string {
|
||||
const folder = vscode.workspace.getWorkspaceFolder(resource);
|
||||
return folder ? folder.uri.toString() : '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import * as path from 'path';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
|
||||
import * as nls from 'vscode-nls';
|
||||
const localize = nls.loadMessageBundle();
|
||||
|
||||
import { Logger } from '../logger';
|
||||
import { ContentSecurityPolicyArbiter, MarkdownPreviewSecurityLevel } from '../security';
|
||||
import { MarkdownPreviewConfigurationManager, MarkdownPreviewConfiguration } from './previewConfig';
|
||||
import { MarkdownContributions } from '../markdownExtensions';
|
||||
|
||||
/**
|
||||
* Strings used inside the markdown preview.
|
||||
*
|
||||
* Stored here and then injected in the preview so that they
|
||||
* can be localized using our normal localization process.
|
||||
*/
|
||||
const previewStrings = {
|
||||
cspAlertMessageText: localize(
|
||||
'preview.securityMessage.text',
|
||||
'Some content has been disabled in this document'),
|
||||
|
||||
cspAlertMessageTitle: localize(
|
||||
'preview.securityMessage.title',
|
||||
'Potentially unsafe or insecure content has been disabled in the markdown preview. Change the Markdown preview security setting to allow insecure content or enable scripts'),
|
||||
|
||||
cspAlertMessageLabel: localize(
|
||||
'preview.securityMessage.label',
|
||||
'Content Disabled Security Warning')
|
||||
};
|
||||
|
||||
export class MarkdownContentProvider {
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine,
|
||||
private readonly context: vscode.ExtensionContext,
|
||||
private readonly cspArbiter: ContentSecurityPolicyArbiter,
|
||||
private readonly contributions: MarkdownContributions,
|
||||
private readonly logger: Logger
|
||||
) { }
|
||||
|
||||
public async provideTextDocumentContent(
|
||||
markdownDocument: vscode.TextDocument,
|
||||
previewConfigurations: MarkdownPreviewConfigurationManager,
|
||||
initialLine: number | undefined = undefined
|
||||
): Promise<string> {
|
||||
const sourceUri = markdownDocument.uri;
|
||||
const config = previewConfigurations.loadAndCacheConfiguration(sourceUri);
|
||||
const initialData = {
|
||||
source: sourceUri.toString(),
|
||||
line: initialLine,
|
||||
lineCount: markdownDocument.lineCount,
|
||||
scrollPreviewWithEditor: config.scrollPreviewWithEditor,
|
||||
scrollEditorWithPreview: config.scrollEditorWithPreview,
|
||||
doubleClickToSwitchToEditor: config.doubleClickToSwitchToEditor,
|
||||
disableSecurityWarnings: this.cspArbiter.shouldDisableSecurityWarnings()
|
||||
};
|
||||
|
||||
this.logger.log('provideTextDocumentContent', initialData);
|
||||
|
||||
// Content Security Policy
|
||||
const nonce = new Date().getTime() + '' + new Date().getMilliseconds();
|
||||
const csp = this.getCspForResource(sourceUri, nonce);
|
||||
|
||||
const body = await this.engine.render(sourceUri, config.previewFrontMatter === 'hide', markdownDocument.getText());
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html;charset=UTF-8">
|
||||
${csp}
|
||||
<meta id="vscode-markdown-preview-data" data-settings="${JSON.stringify(initialData).replace(/"/g, '"')}" data-strings="${JSON.stringify(previewStrings).replace(/"/g, '"')}">
|
||||
<script src="${this.extensionResourcePath('pre.js')}" nonce="${nonce}"></script>
|
||||
${this.getStyles(sourceUri, nonce, config)}
|
||||
<base href="${markdownDocument.uri.with({ scheme: 'vscode-resource' }).toString(true)}">
|
||||
</head>
|
||||
<body class="vscode-body ${config.scrollBeyondLastLine ? 'scrollBeyondLastLine' : ''} ${config.wordWrap ? 'wordWrap' : ''} ${config.markEditorSelection ? 'showEditorSelection' : ''}">
|
||||
${body}
|
||||
<div class="code-line" data-line="${markdownDocument.lineCount}"></div>
|
||||
${this.getScripts(nonce)}
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
private extensionResourcePath(mediaFile: string): string {
|
||||
return vscode.Uri.file(this.context.asAbsolutePath(path.join('media', mediaFile)))
|
||||
.with({ scheme: 'vscode-resource' })
|
||||
.toString();
|
||||
}
|
||||
|
||||
private fixHref(resource: vscode.Uri, href: string): string {
|
||||
if (!href) {
|
||||
return href;
|
||||
}
|
||||
|
||||
// Use href if it is already an URL
|
||||
const hrefUri = vscode.Uri.parse(href);
|
||||
if (['http', 'https'].indexOf(hrefUri.scheme) >= 0) {
|
||||
return hrefUri.toString();
|
||||
}
|
||||
|
||||
// Use href as file URI if it is absolute
|
||||
if (path.isAbsolute(href) || hrefUri.scheme === 'file') {
|
||||
return vscode.Uri.file(href)
|
||||
.with({ scheme: 'vscode-resource' })
|
||||
.toString();
|
||||
}
|
||||
|
||||
// Use a workspace relative path if there is a workspace
|
||||
let root = vscode.workspace.getWorkspaceFolder(resource);
|
||||
if (root) {
|
||||
return vscode.Uri.file(path.join(root.uri.fsPath, href))
|
||||
.with({ scheme: 'vscode-resource' })
|
||||
.toString();
|
||||
}
|
||||
|
||||
// Otherwise look relative to the markdown file
|
||||
return vscode.Uri.file(path.join(path.dirname(resource.fsPath), href))
|
||||
.with({ scheme: 'vscode-resource' })
|
||||
.toString();
|
||||
}
|
||||
|
||||
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, '"')}" href="${this.fixHref(resource, style)}" type="text/css" media="screen">`;
|
||||
}).join('\n');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private getSettingsOverrideStyles(nonce: string, config: MarkdownPreviewConfiguration): string {
|
||||
return `<style nonce="${nonce}">
|
||||
body {
|
||||
${config.fontFamily ? `font-family: ${config.fontFamily};` : ''}
|
||||
${isNaN(config.fontSize) ? '' : `font-size: ${config.fontSize}px;`}
|
||||
${isNaN(config.lineHeight) ? '' : `line-height: ${config.lineHeight};`}
|
||||
}
|
||||
</style>`;
|
||||
}
|
||||
|
||||
private getStyles(resource: vscode.Uri, nonce: string, config: MarkdownPreviewConfiguration): 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)}`;
|
||||
}
|
||||
|
||||
private getScripts(nonce: string): string {
|
||||
return this.contributions.previewScripts
|
||||
.map(resource => `<script async src="${resource.toString()}" nonce="${nonce}" charset="UTF-8"></script>`)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
private getCspForResource(resource: vscode.Uri, nonce: string): string {
|
||||
switch (this.cspArbiter.getSecurityLevelForResource(resource)) {
|
||||
case MarkdownPreviewSecurityLevel.AllowInsecureContent:
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: http: https: data:; media-src vscode-resource: http: https: data:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:; font-src vscode-resource: http: https: data:;">`;
|
||||
|
||||
case MarkdownPreviewSecurityLevel.AllowInsecureLocalContent:
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; media-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' https: data: http://localhost:* http://127.0.0.1:*; font-src vscode-resource: https: data: http://localhost:* http://127.0.0.1:*;">`;
|
||||
|
||||
case MarkdownPreviewSecurityLevel.AllowScriptsAndAllContent:
|
||||
return '';
|
||||
|
||||
case MarkdownPreviewSecurityLevel.Strict:
|
||||
default:
|
||||
return `<meta http-equiv="Content-Security-Policy" content="default-src 'none'; img-src vscode-resource: https: data:; media-src vscode-resource: https: data:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' https: data:; font-src vscode-resource: https: data:;">`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { Logger } from '../logger';
|
||||
import { MarkdownContributions } from '../markdownExtensions';
|
||||
import { disposeAll } from '../util/dispose';
|
||||
import { MarkdownFileTopmostLineMonitor } from '../util/topmostLineMonitor';
|
||||
import { MarkdownPreview, PreviewSettings } from './preview';
|
||||
import { MarkdownPreviewConfigurationManager } from './previewConfig';
|
||||
import { MarkdownContentProvider } from './previewContentProvider';
|
||||
|
||||
|
||||
export class MarkdownPreviewManager implements vscode.WebviewPanelSerializer {
|
||||
private static readonly markdownPreviewActiveContextKey = 'markdownPreviewFocus';
|
||||
|
||||
private readonly topmostLineMonitor = new MarkdownFileTopmostLineMonitor();
|
||||
private readonly previewConfigurations = new MarkdownPreviewConfigurationManager();
|
||||
private readonly previews: MarkdownPreview[] = [];
|
||||
private activePreview: MarkdownPreview | undefined = undefined;
|
||||
private readonly disposables: vscode.Disposable[] = [];
|
||||
|
||||
public constructor(
|
||||
private readonly contentProvider: MarkdownContentProvider,
|
||||
private readonly logger: Logger,
|
||||
private readonly contributions: MarkdownContributions
|
||||
) {
|
||||
this.disposables.push(vscode.window.registerWebviewPanelSerializer(MarkdownPreview.viewType, this));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
disposeAll(this.disposables);
|
||||
disposeAll(this.previews);
|
||||
}
|
||||
|
||||
public refresh() {
|
||||
for (const preview of this.previews) {
|
||||
preview.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
public updateConfiguration() {
|
||||
for (const preview of this.previews) {
|
||||
preview.updateConfiguration();
|
||||
}
|
||||
}
|
||||
|
||||
public preview(
|
||||
resource: vscode.Uri,
|
||||
previewSettings: PreviewSettings
|
||||
): void {
|
||||
let preview = this.getExistingPreview(resource, previewSettings);
|
||||
if (preview) {
|
||||
preview.reveal(previewSettings.previewColumn);
|
||||
} else {
|
||||
preview = this.createNewPreview(resource, previewSettings);
|
||||
}
|
||||
|
||||
preview.update(resource);
|
||||
}
|
||||
|
||||
public get activePreviewResource() {
|
||||
return this.activePreview && this.activePreview.resource;
|
||||
}
|
||||
|
||||
public toggleLock() {
|
||||
const preview = this.activePreview;
|
||||
if (preview) {
|
||||
preview.toggleLock();
|
||||
|
||||
// Close any previews that are now redundant, such as having two dynamic previews in the same editor group
|
||||
for (const otherPreview of this.previews) {
|
||||
if (otherPreview !== preview && preview.matches(otherPreview)) {
|
||||
otherPreview.dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async deserializeWebviewPanel(
|
||||
webview: vscode.WebviewPanel,
|
||||
state: any
|
||||
): Promise<void> {
|
||||
const preview = await MarkdownPreview.revive(
|
||||
webview,
|
||||
state,
|
||||
this.contentProvider,
|
||||
this.previewConfigurations,
|
||||
this.logger,
|
||||
this.topmostLineMonitor);
|
||||
|
||||
this.registerPreview(preview);
|
||||
}
|
||||
|
||||
public async serializeWebviewPanel(
|
||||
webview: vscode.WebviewPanel,
|
||||
): Promise<any> {
|
||||
const preview = this.previews.find(preview => preview.isWebviewOf(webview));
|
||||
return preview ? preview.state : undefined;
|
||||
}
|
||||
|
||||
private getExistingPreview(
|
||||
resource: vscode.Uri,
|
||||
previewSettings: PreviewSettings
|
||||
): MarkdownPreview | undefined {
|
||||
return this.previews.find(preview =>
|
||||
preview.matchesResource(resource, previewSettings.previewColumn, previewSettings.locked));
|
||||
}
|
||||
|
||||
private createNewPreview(
|
||||
resource: vscode.Uri,
|
||||
previewSettings: PreviewSettings
|
||||
): MarkdownPreview {
|
||||
const preview = MarkdownPreview.create(
|
||||
resource,
|
||||
previewSettings.previewColumn,
|
||||
previewSettings.locked,
|
||||
this.contentProvider,
|
||||
this.previewConfigurations,
|
||||
this.logger,
|
||||
this.topmostLineMonitor,
|
||||
this.contributions);
|
||||
|
||||
return this.registerPreview(preview);
|
||||
}
|
||||
|
||||
private registerPreview(
|
||||
preview: MarkdownPreview
|
||||
): MarkdownPreview {
|
||||
this.previews.push(preview);
|
||||
|
||||
preview.onDispose(() => {
|
||||
const existing = this.previews.indexOf(preview!);
|
||||
if (existing >= 0) {
|
||||
this.previews.splice(existing, 1);
|
||||
}
|
||||
});
|
||||
|
||||
preview.onDidChangeViewState(({ webviewPanel }) => {
|
||||
disposeAll(this.previews.filter(otherPreview => preview !== otherPreview && preview!.matches(otherPreview)));
|
||||
|
||||
vscode.commands.executeCommand('setContext', MarkdownPreviewManager.markdownPreviewActiveContextKey,
|
||||
webviewPanel.visible);
|
||||
|
||||
this.activePreview = webviewPanel.visible ? preview : undefined;
|
||||
});
|
||||
|
||||
return preview;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { disposeAll } from '../util/dispose';
|
||||
import { isMarkdownFile } from '../util/file';
|
||||
import { Lazy, lazy } from '../util/lazy';
|
||||
import MDDocumentSymbolProvider from './documentSymbolProvider';
|
||||
|
||||
export interface WorkspaceMarkdownDocumentProvider {
|
||||
getAllMarkdownDocuments(): Thenable<vscode.TextDocument[]>;
|
||||
|
||||
readonly onDidChangeMarkdownDocument: vscode.Event<vscode.TextDocument>;
|
||||
readonly onDidCreateMarkdownDocument: vscode.Event<vscode.TextDocument>;
|
||||
readonly onDidDeleteMarkdownDocument: vscode.Event<vscode.Uri>;
|
||||
}
|
||||
|
||||
class VSCodeWorkspaceMarkdownDocumentProvider implements WorkspaceMarkdownDocumentProvider {
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.TextDocument>();
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.TextDocument>();
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
|
||||
private _watcher: vscode.FileSystemWatcher | undefined;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public dispose() {
|
||||
this._onDidChangeMarkdownDocumentEmitter.dispose();
|
||||
this._onDidDeleteMarkdownDocumentEmitter.dispose();
|
||||
|
||||
if (this._watcher) {
|
||||
this._watcher.dispose();
|
||||
}
|
||||
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
async getAllMarkdownDocuments() {
|
||||
const resources = await vscode.workspace.findFiles('**/*.md', '**/node_modules/**');
|
||||
const documents = await Promise.all(
|
||||
resources.map(resource => vscode.workspace.openTextDocument(resource).then(x => x, () => undefined)));
|
||||
return documents.filter(doc => doc && isMarkdownFile(doc)) as vscode.TextDocument[];
|
||||
}
|
||||
|
||||
public get onDidChangeMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
public get onDidCreateMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
public get onDidDeleteMarkdownDocument() {
|
||||
this.ensureWatcher();
|
||||
return this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
}
|
||||
|
||||
private ensureWatcher(): void {
|
||||
if (this._watcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._watcher = vscode.workspace.createFileSystemWatcher('**/*.md');
|
||||
|
||||
this._watcher.onDidChange(async resource => {
|
||||
const document = await vscode.workspace.openTextDocument(resource);
|
||||
if (isMarkdownFile(document)) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}, null, this._disposables);
|
||||
|
||||
this._watcher.onDidCreate(async resource => {
|
||||
const document = await vscode.workspace.openTextDocument(resource);
|
||||
if (isMarkdownFile(document)) {
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
}, null, this._disposables);
|
||||
|
||||
this._watcher.onDidDelete(async resource => {
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}, null, this._disposables);
|
||||
|
||||
vscode.workspace.onDidChangeTextDocument(e => {
|
||||
if (isMarkdownFile(e.document)) {
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(e.document);
|
||||
}
|
||||
}, null, this._disposables);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default class MarkdownWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
|
||||
private _symbolCache = new Map<string, Lazy<Thenable<vscode.SymbolInformation[]>>>();
|
||||
private _symbolCachePopulated: boolean = false;
|
||||
private _disposables: vscode.Disposable[] = [];
|
||||
|
||||
public constructor(
|
||||
private _symbolProvider: MDDocumentSymbolProvider,
|
||||
private _workspaceMarkdownDocumentProvider: WorkspaceMarkdownDocumentProvider = new VSCodeWorkspaceMarkdownDocumentProvider()
|
||||
) { }
|
||||
|
||||
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
|
||||
if (!this._symbolCachePopulated) {
|
||||
await this.populateSymbolCache();
|
||||
this._symbolCachePopulated = true;
|
||||
|
||||
this._workspaceMarkdownDocumentProvider.onDidChangeMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
|
||||
this._workspaceMarkdownDocumentProvider.onDidCreateMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
|
||||
this._workspaceMarkdownDocumentProvider.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this, this._disposables);
|
||||
}
|
||||
|
||||
const allSymbolsSets = await Promise.all(Array.from(this._symbolCache.values()).map(x => x.value));
|
||||
const allSymbols: vscode.SymbolInformation[] = Array.prototype.concat.apply([], allSymbolsSets);
|
||||
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
|
||||
}
|
||||
|
||||
public async populateSymbolCache(): Promise<void> {
|
||||
const markDownDocumentUris = await this._workspaceMarkdownDocumentProvider.getAllMarkdownDocuments();
|
||||
for (const document of markDownDocumentUris) {
|
||||
this._symbolCache.set(document.fileName, this.getSymbols(document));
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
disposeAll(this._disposables);
|
||||
}
|
||||
|
||||
private getSymbols(document: vscode.TextDocument): Lazy<Thenable<vscode.SymbolInformation[]>> {
|
||||
return lazy(async () => {
|
||||
return this._symbolProvider.provideDocumentSymbols(document);
|
||||
});
|
||||
}
|
||||
|
||||
private onDidChangeDocument(document: vscode.TextDocument) {
|
||||
this._symbolCache.set(document.fileName, this.getSymbols(document));
|
||||
}
|
||||
|
||||
private onDidDeleteDocument(resource: vscode.Uri) {
|
||||
this._symbolCache.delete(resource.fsPath);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user