Merge from vscode merge-base (#22780)

* Revert "Revert "Merge from vscode merge-base (#22769)" (#22779)"

This reverts commit 47a1745180.

* Fix notebook download task

* Remove done call from extensions-ci
This commit is contained in:
Karl Burtram
2023-04-19 21:48:46 -07:00
committed by GitHub
parent decbe8dded
commit e7d3d047ec
2389 changed files with 92155 additions and 42602 deletions

View File

@@ -0,0 +1,29 @@
/*---------------------------------------------------------------------------------------------
* 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';
import { tryGetUriListSnippet } from './dropIntoEditor';
export function registerPasteSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentPasteEditProvider(selector, new class implements vscode.DocumentPasteEditProvider {
async provideDocumentPasteEdits(
document: vscode.TextDocument,
_ranges: readonly vscode.Range[],
dataTransfer: vscode.DataTransfer,
token: vscode.CancellationToken,
): Promise<vscode.DocumentPasteEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('experimental.editor.pasteLinks.enabled', true);
if (!enabled) {
return;
}
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentPasteEdit(snippet) : undefined;
}
}, {
pasteMimeTypes: ['text/uri-list']
});
}

View File

@@ -1,21 +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 vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { SkinnyTextDocument } from '../workspaceContents';
import { MdReferencesProvider } from './references';
export class MdDefinitionProvider extends Disposable implements vscode.DefinitionProvider {
constructor(private readonly referencesProvider: MdReferencesProvider) {
super();
}
async provideDefinition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<vscode.Definition | undefined> {
const allRefs = await this.referencesProvider.getAllReferencesAtPosition(document, position, token);
return allRefs.find(ref => ref.kind === 'link' && ref.isDefinition)?.location;
}
}

View File

@@ -5,294 +5,79 @@
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { noopToken } from '../test/util';
import { Delayer } from '../util/async';
import { Disposable } from '../util/dispose';
import { isMarkdownFile } from '../util/file';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, LinkDefinitionSet, MdLink, MdLinkProvider } from './documentLinkProvider';
import { tryFindMdDocumentForLink } from './references';
import { CommandManager } from '../commandManager';
const localize = nls.loadMessageBundle();
export interface DiagnosticConfiguration {
/**
* Fired when the configuration changes.
*/
readonly onDidChange: vscode.Event<void>;
getOptions(resource: vscode.Uri): DiagnosticOptions;
// Copied from markdown language service
export enum DiagnosticCode {
link_noSuchReferences = 'link.no-such-reference',
link_noSuchHeaderInOwnFile = 'link.no-such-header-in-own-file',
link_noSuchFile = 'link.no-such-file',
link_noSuchHeaderInFile = 'link.no-such-header-in-file',
}
export enum DiagnosticLevel {
ignore = 'ignore',
warning = 'warning',
error = 'error',
}
export interface DiagnosticOptions {
readonly enabled: boolean;
readonly validateReferences: DiagnosticLevel;
readonly validateOwnHeaders: DiagnosticLevel;
readonly validateFilePaths: DiagnosticLevel;
}
class AddToIgnoreLinksQuickFixProvider implements vscode.CodeActionProvider {
function toSeverity(level: DiagnosticLevel): vscode.DiagnosticSeverity | undefined {
switch (level) {
case DiagnosticLevel.error: return vscode.DiagnosticSeverity.Error;
case DiagnosticLevel.warning: return vscode.DiagnosticSeverity.Warning;
case DiagnosticLevel.ignore: return undefined;
}
}
private static readonly _addToIgnoreLinksCommandId = '_markdown.addToIgnoreLinks';
class VSCodeDiagnosticConfiguration extends Disposable implements DiagnosticConfiguration {
private static readonly metadata: vscode.CodeActionProviderMetadata = {
providedCodeActionKinds: [
vscode.CodeActionKind.QuickFix
],
};
private readonly _onDidChange = this._register(new vscode.EventEmitter<void>());
public readonly onDidChange = this._onDidChange.event;
constructor() {
super();
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('markdown.experimental.validate.enabled')) {
this._onDidChange.fire();
public static register(selector: vscode.DocumentSelector, commandManager: CommandManager): vscode.Disposable {
const reg = vscode.languages.registerCodeActionsProvider(selector, new AddToIgnoreLinksQuickFixProvider(), AddToIgnoreLinksQuickFixProvider.metadata);
const commandReg = commandManager.register({
id: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
execute(resource: vscode.Uri, path: string) {
const settingId = 'experimental.validate.ignoreLinks';
const config = vscode.workspace.getConfiguration('markdown', resource);
const paths = new Set(config.get<string[]>(settingId, []));
paths.add(path);
config.update(settingId, [...paths], vscode.ConfigurationTarget.WorkspaceFolder);
}
}));
});
return vscode.Disposable.from(reg, commandReg);
}
public getOptions(resource: vscode.Uri): DiagnosticOptions {
const config = vscode.workspace.getConfiguration('markdown', resource);
return {
enabled: config.get<boolean>('experimental.validate.enabled', false),
validateReferences: config.get<DiagnosticLevel>('experimental.validate.referenceLinks', DiagnosticLevel.ignore),
validateOwnHeaders: config.get<DiagnosticLevel>('experimental.validate.headerLinks', DiagnosticLevel.ignore),
validateFilePaths: config.get<DiagnosticLevel>('experimental.validate.fileLinks', DiagnosticLevel.ignore),
};
}
}
provideCodeActions(document: vscode.TextDocument, _range: vscode.Range | vscode.Selection, context: vscode.CodeActionContext, _token: vscode.CancellationToken): vscode.ProviderResult<(vscode.CodeAction | vscode.Command)[]> {
const fixes: vscode.CodeAction[] = [];
export class DiagnosticManager extends Disposable {
for (const diagnostic of context.diagnostics) {
switch (diagnostic.code) {
case DiagnosticCode.link_noSuchReferences:
case DiagnosticCode.link_noSuchHeaderInOwnFile:
case DiagnosticCode.link_noSuchFile:
case DiagnosticCode.link_noSuchHeaderInFile: {
const hrefText = (diagnostic as any).data?.hrefText;
if (hrefText) {
const fix = new vscode.CodeAction(
localize('ignoreLinksQuickFix.title', "Exclude '{0}' from link validation.", hrefText),
vscode.CodeActionKind.QuickFix);
private readonly collection: vscode.DiagnosticCollection;
private readonly pendingDiagnostics = new Set<vscode.Uri>();
private readonly diagnosticDelayer: Delayer<void>;
constructor(
private readonly computer: DiagnosticComputer,
private readonly configuration: DiagnosticConfiguration,
) {
super();
this.diagnosticDelayer = new Delayer(300);
this.collection = this._register(vscode.languages.createDiagnosticCollection('markdown'));
this._register(this.configuration.onDidChange(() => {
this.rebuild();
}));
const onDocUpdated = (doc: vscode.TextDocument) => {
if (isMarkdownFile(doc)) {
this.pendingDiagnostics.add(doc.uri);
this.diagnosticDelayer.trigger(() => this.recomputePendingDiagnostics());
}
};
this._register(vscode.workspace.onDidOpenTextDocument(doc => {
onDocUpdated(doc);
}));
this._register(vscode.workspace.onDidChangeTextDocument(e => {
onDocUpdated(e.document);
}));
this._register(vscode.workspace.onDidCloseTextDocument(doc => {
this.pendingDiagnostics.delete(doc.uri);
this.collection.delete(doc.uri);
}));
this.rebuild();
}
private recomputePendingDiagnostics(): void {
const pending = [...this.pendingDiagnostics];
this.pendingDiagnostics.clear();
for (const resource of pending) {
const doc = vscode.workspace.textDocuments.find(doc => doc.uri.fsPath === resource.fsPath);
if (doc) {
this.update(doc);
}
}
}
private async rebuild() {
this.collection.clear();
const allOpenedTabResources = this.getAllTabResources();
await Promise.all(
vscode.workspace.textDocuments
.filter(doc => allOpenedTabResources.has(doc.uri.toString()) && isMarkdownFile(doc))
.map(doc => this.update(doc)));
}
private getAllTabResources() {
const openedTabDocs = new Map<string, vscode.Uri>();
for (const group of vscode.window.tabGroups.all) {
for (const tab of group.tabs) {
if (tab.input instanceof vscode.TabInputText) {
openedTabDocs.set(tab.input.uri.toString(), tab.input.uri);
}
}
}
return openedTabDocs;
}
private async update(doc: vscode.TextDocument): Promise<void> {
const diagnostics = await this.getDiagnostics(doc, noopToken);
this.collection.set(doc.uri, diagnostics);
}
public async getDiagnostics(doc: SkinnyTextDocument, token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const config = this.configuration.getOptions(doc.uri);
if (!config.enabled) {
return [];
}
return this.computer.getDiagnostics(doc, config, token);
}
}
export class DiagnosticComputer {
constructor(
private readonly engine: MarkdownEngine,
private readonly workspaceContents: MdWorkspaceContents,
private readonly linkProvider: MdLinkProvider,
) { }
public async getDiagnostics(doc: SkinnyTextDocument, options: DiagnosticOptions, token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const links = await this.linkProvider.getAllLinks(doc, token);
if (token.isCancellationRequested) {
return [];
}
return (await Promise.all([
this.validateFileLinks(doc, options, links, token),
Array.from(this.validateReferenceLinks(options, links)),
this.validateOwnHeaderLinks(doc, options, links, token),
])).flat();
}
private async validateOwnHeaderLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateOwnHeaders);
if (typeof severity === 'undefined') {
return [];
}
const toc = await TableOfContents.create(this.engine, doc);
if (token.isCancellationRequested) {
return [];
}
const diagnostics: vscode.Diagnostic[] = [];
for (const link of links) {
if (link.href.kind === 'internal'
&& link.href.path.toString() === doc.uri.toString()
&& link.href.fragment
&& !toc.lookup(link.href.fragment)
) {
diagnostics.push(new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidHeaderLink', 'No header found: \'{0}\'', link.href.fragment),
severity));
}
}
return diagnostics;
}
private *validateReferenceLinks(options: DiagnosticOptions, links: readonly MdLink[]): Iterable<vscode.Diagnostic> {
const severity = toSeverity(options.validateReferences);
if (typeof severity === 'undefined') {
return [];
}
const definitionSet = new LinkDefinitionSet(links);
for (const link of links) {
if (link.href.kind === 'reference' && !definitionSet.lookup(link.href.ref)) {
yield new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidReferenceLink', 'No link reference found: \'{0}\'', link.href.ref),
severity);
}
}
}
private async validateFileLinks(doc: SkinnyTextDocument, options: DiagnosticOptions, links: readonly MdLink[], token: vscode.CancellationToken): Promise<vscode.Diagnostic[]> {
const severity = toSeverity(options.validateFilePaths);
if (typeof severity === 'undefined') {
return [];
}
const tocs = new Map<string, TableOfContents>();
// TODO: cache links so we don't recompute duplicate hrefs
// TODO: parallelize
const diagnostics: vscode.Diagnostic[] = [];
for (const link of links) {
if (token.isCancellationRequested) {
return [];
}
if (link.href.kind !== 'internal') {
continue;
}
const hrefDoc = await tryFindMdDocumentForLink(link.href, this.workspaceContents);
if (hrefDoc && hrefDoc.uri.toString() === doc.uri.toString()) {
continue;
}
if (!hrefDoc && !await this.workspaceContents.pathExists(link.href.path)) {
diagnostics.push(
new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidPathLink', 'File does not exist at path: {0}', (link.href as InternalHref).path.toString(true)),
severity));
} else if (hrefDoc) {
if (link.href.fragment) {
// validate fragment looks valid
let hrefDocToc = tocs.get(link.href.path.toString());
if (!hrefDocToc) {
hrefDocToc = await TableOfContents.create(this.engine, hrefDoc);
tocs.set(link.href.path.toString(), hrefDocToc);
}
if (!hrefDocToc.lookup(link.href.fragment)) {
diagnostics.push(
new vscode.Diagnostic(
link.source.hrefRange,
localize('invalidLinkToHeaderInOtherFile', 'Header does not exist in file: {0}', (link.href as InternalHref).path.fragment),
severity));
fix.command = {
command: AddToIgnoreLinksQuickFixProvider._addToIgnoreLinksCommandId,
title: '',
arguments: [document.uri, hrefText],
};
fixes.push(fix);
}
break;
}
}
}
return diagnostics;
return fixes;
}
}
export function register(
engine: MarkdownEngine,
workspaceContents: MdWorkspaceContents,
linkProvider: MdLinkProvider,
export function registerDiagnosticSupport(
selector: vscode.DocumentSelector,
commandManager: CommandManager,
): vscode.Disposable {
const configuration = new VSCodeDiagnosticConfiguration();
const manager = new DiagnosticManager(new DiagnosticComputer(engine, workspaceContents, linkProvider), configuration);
return vscode.Disposable.from(configuration, manager);
return AddToIgnoreLinksQuickFixProvider.register(selector, commandManager);
}

View File

@@ -1,421 +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 vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as uri from 'vscode-uri';
import { OpenDocumentLinkCommand } from '../commands/openDocumentLink';
import { MarkdownEngine } from '../markdownEngine';
import { coalesce } from '../util/arrays';
import { getUriForLinkWithKnownExternalScheme, isOfScheme, Schemes } from '../util/schemes';
import { SkinnyTextDocument } from '../workspaceContents';
const localize = nls.loadMessageBundle();
export interface ExternalHref {
readonly kind: 'external';
readonly uri: vscode.Uri;
}
export interface InternalHref {
readonly kind: 'internal';
readonly path: vscode.Uri;
readonly fragment: string;
}
export interface ReferenceHref {
readonly kind: 'reference';
readonly ref: string;
}
export type LinkHref = ExternalHref | InternalHref | ReferenceHref;
function parseLink(
document: SkinnyTextDocument,
link: string,
): ExternalHref | InternalHref | undefined {
const cleanLink = stripAngleBrackets(link);
const externalSchemeUri = getUriForLinkWithKnownExternalScheme(cleanLink);
if (externalSchemeUri) {
// Normalize VS Code links to target currently running version
if (isOfScheme(Schemes.vscode, link) || isOfScheme(Schemes['vscode-insiders'], link)) {
return { kind: 'external', uri: vscode.Uri.parse(link).with({ scheme: vscode.env.uriScheme }) };
}
return { kind: 'external', uri: externalSchemeUri };
}
// 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 resourceUri: vscode.Uri | undefined;
if (!tempUri.path) {
resourceUri = document.uri;
} else if (tempUri.path[0] === '/') {
const root = getWorkspaceFolder(document);
if (root) {
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
}
} else {
if (document.uri.scheme === Schemes.untitled) {
const root = getWorkspaceFolder(document);
if (root) {
resourceUri = vscode.Uri.joinPath(root, tempUri.path);
}
} else {
const base = uri.Utils.dirname(document.uri);
resourceUri = vscode.Uri.joinPath(base, tempUri.path);
}
}
if (!resourceUri) {
return undefined;
}
return {
kind: 'internal',
path: resourceUri.with({ fragment: '' }),
fragment: tempUri.fragment,
};
}
function getWorkspaceFolder(document: SkinnyTextDocument) {
return vscode.workspace.getWorkspaceFolder(document.uri)?.uri
|| vscode.workspace.workspaceFolders?.[0]?.uri;
}
interface MdLinkSource {
readonly text: string;
readonly resource: vscode.Uri;
readonly hrefRange: vscode.Range;
readonly fragmentRange: vscode.Range | undefined;
}
export interface MdInlineLink {
readonly kind: 'link';
readonly source: MdLinkSource;
readonly href: LinkHref;
}
export interface MdLinkDefinition {
readonly kind: 'definition';
readonly source: MdLinkSource;
readonly ref: {
readonly range: vscode.Range;
readonly text: string;
};
readonly href: ExternalHref | InternalHref;
}
export type MdLink = MdInlineLink | MdLinkDefinition;
function extractDocumentLink(
document: SkinnyTextDocument,
pre: number,
link: string,
matchIndex: number | undefined
): MdLink | undefined {
const offset = (matchIndex || 0) + pre;
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
try {
const linkTarget = parseLink(document, link);
if (!linkTarget) {
return undefined;
}
return {
kind: 'link',
href: linkTarget,
source: {
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
}
};
} catch {
return undefined;
}
}
function getFragmentRange(text: string, start: vscode.Position, end: vscode.Position): vscode.Range | undefined {
const index = text.indexOf('#');
if (index < 0) {
return undefined;
}
return new vscode.Range(start.translate({ characterDelta: index + 1 }), end);
}
const angleBracketLinkRe = /^<(.*)>$/;
/**
* Used to strip brackets from the markdown link
*
* <http://example.com> will be transformed to http://example.com
*/
function stripAngleBrackets(link: string) {
return link.replace(angleBracketLinkRe, '$1');
}
/**
* Matches `[text](link)`
*/
const linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g;
/**
* Matches `[text][ref]`
*/
const referenceLinkPattern = /(?:(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]|\[\s*?([^\s\]]*?)\])(?![\:\(])/g;
/**
* Matches `<http://example.com>`
*/
const autoLinkPattern = /\<(\w+:[^\>\s]+)\>/g;
/**
* Matches `[text]: link`
*/
const definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)([^<]\S*|<[^>]+>)/gm;
const inlineCodePattern = /(?:^|[^`])(`+)(?:.+?|.*?(?:(?:\r?\n).+?)*?)(?:\r?\n)?\1(?:$|[^`])/gm;
interface CodeInDocument {
/**
* code blocks and fences each represented by [line_start,line_end).
*/
readonly multiline: ReadonlyArray<[number, number]>;
/**
* inline code spans each represented by {@link vscode.Range}.
*/
readonly inline: readonly vscode.Range[];
}
async function findCode(document: SkinnyTextDocument, engine: MarkdownEngine): Promise<CodeInDocument> {
const tokens = await engine.parse(document);
const multiline = tokens.filter(t => (t.type === 'code_block' || t.type === 'fence') && !!t.map).map(t => t.map) as [number, number][];
const text = document.getText();
const inline = [...text.matchAll(inlineCodePattern)].map(match => {
const start = match.index || 0;
return new vscode.Range(document.positionAt(start), document.positionAt(start + match[0].length));
});
return { multiline, inline };
}
function isLinkInsideCode(code: CodeInDocument, linkHrefRange: vscode.Range) {
return code.multiline.some(interval => linkHrefRange.start.line >= interval[0] && linkHrefRange.start.line < interval[1]) ||
code.inline.some(position => position.intersection(linkHrefRange));
}
export class MdLinkProvider implements vscode.DocumentLinkProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideDocumentLinks(
document: SkinnyTextDocument,
token: vscode.CancellationToken
): Promise<vscode.DocumentLink[]> {
const allLinks = await this.getAllLinks(document, token);
if (token.isCancellationRequested) {
return [];
}
const definitionSet = new LinkDefinitionSet(allLinks);
return coalesce(allLinks
.map(data => this.toValidDocumentLink(data, definitionSet)));
}
private toValidDocumentLink(link: MdLink, definitionSet: LinkDefinitionSet): vscode.DocumentLink | undefined {
switch (link.href.kind) {
case 'external': {
return new vscode.DocumentLink(link.source.hrefRange, link.href.uri);
}
case 'internal': {
const uri = OpenDocumentLinkCommand.createCommandUri(link.source.resource, link.href.path, link.href.fragment);
const documentLink = new vscode.DocumentLink(link.source.hrefRange, uri);
documentLink.tooltip = localize('documentLink.tooltip', 'Follow link');
return documentLink;
}
case 'reference': {
const def = definitionSet.lookup(link.href.ref);
if (def) {
return new vscode.DocumentLink(
link.source.hrefRange,
vscode.Uri.parse(`command:_markdown.moveCursorToPosition?${encodeURIComponent(JSON.stringify([def.source.hrefRange.start.line, def.source.hrefRange.start.character]))}`));
} else {
return undefined;
}
}
}
}
public async getAllLinks(document: SkinnyTextDocument, token: vscode.CancellationToken): Promise<MdLink[]> {
const codeInDocument = await findCode(document, this.engine);
if (token.isCancellationRequested) {
return [];
}
return Array.from([
...this.getInlineLinks(document, codeInDocument),
...this.getReferenceLinks(document, codeInDocument),
...this.getLinkDefinitions2(document, codeInDocument),
...this.getAutoLinks(document, codeInDocument),
]);
}
private *getInlineLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(linkPattern)) {
const matchImageData = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
if (matchImageData && !isLinkInsideCode(codeInDocument, matchImageData.source.hrefRange)) {
yield matchImageData;
}
const matchLinkData = extractDocumentLink(document, match[1].length, match[5], match.index);
if (matchLinkData && !isLinkInsideCode(codeInDocument, matchLinkData.source.hrefRange)) {
yield matchLinkData;
}
}
}
private *getAutoLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(autoLinkPattern)) {
const link = match[1];
const linkTarget = parseLink(document, link);
if (linkTarget) {
const offset = (match.index ?? 0) + 1;
const linkStart = document.positionAt(offset);
const linkEnd = document.positionAt(offset + link.length);
const hrefRange = new vscode.Range(linkStart, linkEnd);
if (isLinkInsideCode(codeInDocument, hrefRange)) {
continue;
}
yield {
kind: 'link',
href: linkTarget,
source: {
text: link,
resource: document.uri,
hrefRange: new vscode.Range(linkStart, linkEnd),
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
}
};
}
}
}
private *getReferenceLinks(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLink> {
const text = document.getText();
for (const match of text.matchAll(referenceLinkPattern)) {
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[4]) { // [ref][], [ref]
reference = match[4];
const offset = (match.index || 0) + 1;
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + reference.length);
} else {
continue;
}
const hrefRange = new vscode.Range(linkStart, linkEnd);
if (isLinkInsideCode(codeInDocument, hrefRange)) {
continue;
}
yield {
kind: 'link',
source: {
text: reference,
resource: document.uri,
hrefRange,
fragmentRange: undefined,
},
href: {
kind: 'reference',
ref: reference,
}
};
}
}
public async getLinkDefinitions(document: SkinnyTextDocument): Promise<Iterable<MdLinkDefinition>> {
const codeInDocument = await findCode(document, this.engine);
return this.getLinkDefinitions2(document, codeInDocument);
}
private *getLinkDefinitions2(document: SkinnyTextDocument, codeInDocument: CodeInDocument): Iterable<MdLinkDefinition> {
const text = document.getText();
for (const match of text.matchAll(definitionPattern)) {
const pre = match[1];
const reference = match[2];
const link = match[3].trim();
const offset = (match.index || 0) + pre.length;
const refStart = document.positionAt((match.index ?? 0) + 1);
const refRange = new vscode.Range(refStart, refStart.translate({ characterDelta: reference.length }));
let linkStart: vscode.Position;
let linkEnd: vscode.Position;
let text: string;
if (angleBracketLinkRe.test(link)) {
linkStart = document.positionAt(offset + 1);
linkEnd = document.positionAt(offset + link.length - 1);
text = link.substring(1, link.length - 1);
} else {
linkStart = document.positionAt(offset);
linkEnd = document.positionAt(offset + link.length);
text = link;
}
const hrefRange = new vscode.Range(linkStart, linkEnd);
if (isLinkInsideCode(codeInDocument, hrefRange)) {
continue;
}
const target = parseLink(document, text);
if (target) {
yield {
kind: 'definition',
source: {
text: link,
resource: document.uri,
hrefRange,
fragmentRange: getFragmentRange(link, linkStart, linkEnd),
},
ref: { text: reference, range: refRange },
href: target,
};
}
}
}
}
export class LinkDefinitionSet {
private readonly _map = new Map<string, MdLinkDefinition>();
constructor(links: Iterable<MdLink>) {
for (const link of links) {
if (link.kind === 'definition') {
this._map.set(link.ref.text, link);
}
}
}
public lookup(ref: string): MdLinkDefinition | undefined {
return this._map.get(ref);
}
}

View File

@@ -1,76 +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 vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { SkinnyTextDocument } from '../workspaceContents';
interface MarkdownSymbol {
readonly level: number;
readonly parent: MarkdownSymbol | undefined;
readonly children: vscode.DocumentSymbol[];
}
export class MdDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideDocumentSymbolInformation(document: SkinnyTextDocument): Promise<vscode.SymbolInformation[]> {
const toc = await TableOfContents.create(this.engine, document);
return toc.entries.map(entry => this.toSymbolInformation(entry));
}
public async provideDocumentSymbols(document: SkinnyTextDocument): Promise<vscode.DocumentSymbol[]> {
const toc = await TableOfContents.create(this.engine, document);
const root: MarkdownSymbol = {
level: -Infinity,
children: [],
parent: undefined
};
this.buildTree(root, toc.entries);
return root.children;
}
private buildTree(parent: MarkdownSymbol, entries: readonly TocEntry[]) {
if (!entries.length) {
return;
}
const entry = entries[0];
const symbol = this.toDocumentSymbol(entry);
symbol.children = [];
while (parent && entry.level <= parent.level) {
parent = parent.parent!;
}
parent.children.push(symbol);
this.buildTree({ level: entry.level, children: symbol.children, parent }, entries.slice(1));
}
private toSymbolInformation(entry: TocEntry): vscode.SymbolInformation {
return new vscode.SymbolInformation(
this.getSymbolName(entry),
vscode.SymbolKind.String,
'',
entry.sectionLocation);
}
private toDocumentSymbol(entry: TocEntry) {
return new vscode.DocumentSymbol(
this.getSymbolName(entry),
'',
vscode.SymbolKind.String,
entry.sectionLocation.range,
entry.sectionLocation.range);
}
private getSymbolName(entry: TocEntry): string {
return '#'.repeat(entry.level) + ' ' + entry.text;
}
}

View File

@@ -23,49 +23,54 @@ const imageFileExtensions = new Set<string>([
'.webp',
]);
export function registerDropIntoEditor(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentOnDropProvider(selector, new class implements vscode.DocumentOnDropProvider {
async provideDocumentOnDropEdits(document: vscode.TextDocument, position: vscode.Position, dataTransfer: vscode.DataTransfer, _token: vscode.CancellationToken): Promise<vscode.SnippetTextEdit | undefined> {
export function registerDropIntoEditorSupport(selector: vscode.DocumentSelector) {
return vscode.languages.registerDocumentDropEditProvider(selector, new class implements vscode.DocumentDropEditProvider {
async provideDocumentDropEdits(document: vscode.TextDocument, _position: vscode.Position, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.DocumentDropEdit | undefined> {
const enabled = vscode.workspace.getConfiguration('markdown', document).get('editor.drop.enabled', true);
if (!enabled) {
return;
}
const urlList = await dataTransfer.get('text/uri-list')?.asString();
if (!urlList) {
return undefined;
}
const uris: vscode.Uri[] = [];
for (const resource of urlList.split('\n')) {
try {
uris.push(vscode.Uri.parse(resource));
} catch {
// noop
}
}
if (!uris.length) {
return;
}
const snippet = new vscode.SnippetString();
uris.forEach((uri, i) => {
const mdPath = document.uri.scheme === uri.scheme
? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath))
: uri.toString(false);
const ext = URI.Utils.extname(uri).toLowerCase();
snippet.appendText(imageFileExtensions.has(ext) ? '![' : '[');
snippet.appendTabstop();
snippet.appendText(`](${mdPath})`);
if (i <= uris.length - 1 && uris.length > 1) {
snippet.appendText(' ');
}
});
return new vscode.SnippetTextEdit(new vscode.Range(position, position), snippet);
const snippet = await tryGetUriListSnippet(document, dataTransfer, token);
return snippet ? new vscode.DocumentDropEdit(snippet) : undefined;
}
});
}
export async function tryGetUriListSnippet(document: vscode.TextDocument, dataTransfer: vscode.DataTransfer, token: vscode.CancellationToken): Promise<vscode.SnippetString | undefined> {
const urlList = await dataTransfer.get('text/uri-list')?.asString();
if (!urlList || token.isCancellationRequested) {
return undefined;
}
const uris: vscode.Uri[] = [];
for (const resource of urlList.split('\n')) {
try {
uris.push(vscode.Uri.parse(resource));
} catch {
// noop
}
}
if (!uris.length) {
return;
}
const snippet = new vscode.SnippetString();
uris.forEach((uri, i) => {
const mdPath = document.uri.scheme === uri.scheme
? encodeURI(path.relative(URI.Utils.dirname(document.uri).fsPath, uri.fsPath).replace(/\\/g, '/'))
: uri.toString(false);
const ext = URI.Utils.extname(uri).toLowerCase();
snippet.appendText(imageFileExtensions.has(ext) ? '![' : '[');
snippet.appendTabstop();
snippet.appendText(`](${mdPath})`);
if (i <= uris.length - 1 && uris.length > 1) {
snippet.appendText(' ');
}
});
return snippet;
}

View File

@@ -4,9 +4,10 @@
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { BaseLanguageClient } from 'vscode-languageclient';
import * as nls from 'vscode-nls';
import { Command, CommandManager } from '../commandManager';
import { MdReferencesProvider } from './references';
import { getReferencesToFileInWorkspace } from '../protocol';
const localize = nls.loadMessageBundle();
@@ -16,7 +17,7 @@ export class FindFileReferencesCommand implements Command {
public readonly id = 'markdown.findAllFileReferences';
constructor(
private readonly referencesProvider: MdReferencesProvider,
private readonly client: BaseLanguageClient,
) { }
public async execute(resource?: vscode.Uri) {
@@ -33,8 +34,9 @@ export class FindFileReferencesCommand implements Command {
location: vscode.ProgressLocation.Window,
title: localize('progress.title', "Finding file references")
}, async (_progress, token) => {
const references = await this.referencesProvider.getAllReferencesToFile(resource!, token);
const locations = references.map(ref => ref.location);
const locations = (await this.client.sendRequest(getReferencesToFileInWorkspace, { uri: resource!.toString() }, token)).map(loc => {
return new vscode.Location(vscode.Uri.parse(loc.uri), new vscode.Range(loc.range.start.line, loc.range.start.character, loc.range.end.line, loc.range.end.character));
});
const config = vscode.workspace.getConfiguration('references');
const existingSetting = config.inspect<string>('preferredLocation');
@@ -49,6 +51,9 @@ export class FindFileReferencesCommand implements Command {
}
}
export function registerFindFileReferences(commandManager: CommandManager, referencesProvider: MdReferencesProvider): vscode.Disposable {
return commandManager.register(new FindFileReferencesCommand(referencesProvider));
export function registerFindFileReferenceSupport(
commandManager: CommandManager,
client: BaseLanguageClient,
): vscode.Disposable {
return commandManager.register(new FindFileReferencesCommand(client));
}

View File

@@ -1,113 +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 Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { SkinnyTextDocument } from '../workspaceContents';
const rangeLimit = 5000;
interface MarkdownItTokenWithMap extends Token {
map: [number, number];
}
export class MdFoldingProvider implements vscode.FoldingRangeProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideFoldingRanges(
document: SkinnyTextDocument,
_: vscode.FoldingContext,
_token: vscode.CancellationToken
): Promise<vscode.FoldingRange[]> {
const foldables = await Promise.all([
this.getRegions(document),
this.getHeaderFoldingRanges(document),
this.getBlockFoldingRanges(document)
]);
return foldables.flat().slice(0, rangeLimit);
}
private async getRegions(document: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
const tokens = await this.engine.parse(document);
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);
}
private async getHeaderFoldingRanges(document: SkinnyTextDocument) {
const toc = await TableOfContents.create(this.engine, document);
return toc.entries.map(entry => {
let endLine = entry.sectionLocation.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: SkinnyTextDocument): Promise<vscode.FoldingRange[]> {
const tokens = await this.engine.parse(document);
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, this.getFoldingRangeKind(listItem));
});
}
private getFoldingRangeKind(listItem: Token): vscode.FoldingRangeKind | undefined {
return listItem.type === 'html_block' && listItem.content.startsWith('<!--')
? vscode.FoldingRangeKind.Comment
: undefined;
}
}
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 is MarkdownItTokenWithMap =>
!!token.map && token.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content));
const isFoldableToken = (token: Token): token is MarkdownItTokenWithMap => {
if (!token.map) {
return false;
}
switch (token.type) {
case 'fence':
case 'list_item_open':
return token.map[1] > token.map[0];
case 'html_block':
if (isRegionMarker(token)) {
return false;
}
return token.map[1] > token.map[0] + 1;
default:
return false;
}
};

View File

@@ -1,353 +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 { dirname, resolve } from 'path';
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents } from '../tableOfContents';
import { resolveUriToMarkdownFile } from '../util/openDocumentLink';
import { SkinnyTextDocument } from '../workspaceContents';
import { MdLinkProvider } from './documentLinkProvider';
enum CompletionContextKind {
/** `[...](|)` */
Link,
/** `[...][|]` */
ReferenceLink,
/** `[]: |` */
LinkDefinition,
}
interface AnchorContext {
/**
* Link text before the `#`.
*
* For `[text](xy#z|abc)` this is `xy`.
*/
readonly beforeAnchor: string;
/**
* Text of the anchor before the current position.
*
* For `[text](xy#z|abc)` this is `z`.
*/
readonly anchorPrefix: string;
}
interface CompletionContext {
readonly kind: CompletionContextKind;
/**
* Text of the link before the current position
*
* For `[text](xy#z|abc)` this is `xy#z`.
*/
readonly linkPrefix: string;
/**
* Position of the start of the link.
*
* For `[text](xy#z|abc)` this is the position before `xy`.
*/
readonly linkTextStartPosition: vscode.Position;
/**
* Text of the link after the current position.
*
* For `[text](xy#z|abc)` this is `abc`.
*/
readonly linkSuffix: string;
/**
* Info if the link looks like it is for an anchor: `[](#header)`
*/
readonly anchorInfo?: AnchorContext;
}
function tryDecodeUriComponent(str: string): string {
try {
return decodeURIComponent(str);
} catch {
return str;
}
}
export class MdPathCompletionProvider implements vscode.CompletionItemProvider {
public static register(
selector: vscode.DocumentSelector,
engine: MarkdownEngine,
linkProvider: MdLinkProvider,
): vscode.Disposable {
return vscode.languages.registerCompletionItemProvider(selector, new MdPathCompletionProvider(engine, linkProvider), '.', '/', '#');
}
constructor(
private readonly engine: MarkdownEngine,
private readonly linkProvider: MdLinkProvider,
) { }
public async provideCompletionItems(document: SkinnyTextDocument, position: vscode.Position, _token: vscode.CancellationToken, _context: vscode.CompletionContext): Promise<vscode.CompletionItem[]> {
if (!this.arePathSuggestionEnabled(document)) {
return [];
}
const context = this.getPathCompletionContext(document, position);
if (!context) {
return [];
}
switch (context.kind) {
case CompletionContextKind.ReferenceLink: {
const items: vscode.CompletionItem[] = [];
for await (const item of this.provideReferenceSuggestions(document, position, context)) {
items.push(item);
}
return items;
}
case CompletionContextKind.LinkDefinition:
case CompletionContextKind.Link: {
const items: vscode.CompletionItem[] = [];
const isAnchorInCurrentDoc = context.anchorInfo && context.anchorInfo.beforeAnchor.length === 0;
// Add anchor #links in current doc
if (context.linkPrefix.length === 0 || isAnchorInCurrentDoc) {
const insertRange = new vscode.Range(context.linkTextStartPosition, position);
for await (const item of this.provideHeaderSuggestions(document, position, context, insertRange)) {
items.push(item);
}
}
if (!isAnchorInCurrentDoc) {
if (context.anchorInfo) { // Anchor to a different document
const rawUri = this.resolveReference(document, context.anchorInfo.beforeAnchor);
if (rawUri) {
const otherDoc = await resolveUriToMarkdownFile(rawUri);
if (otherDoc) {
const anchorStartPosition = position.translate({ characterDelta: -(context.anchorInfo.anchorPrefix.length + 1) });
const range = new vscode.Range(anchorStartPosition, position);
for await (const item of this.provideHeaderSuggestions(otherDoc, position, context, range)) {
items.push(item);
}
}
}
} else { // Normal path suggestions
for await (const item of this.providePathSuggestions(document, position, context)) {
items.push(item);
}
}
}
return items;
}
}
}
private arePathSuggestionEnabled(document: SkinnyTextDocument): boolean {
const config = vscode.workspace.getConfiguration('markdown', document.uri);
return config.get('suggest.paths.enabled', true);
}
/// [...](...|
private readonly linkStartPattern = /\[([^\]]*?)\]\(\s*([^\s\(\)]*)$/;
/// [...][...|
private readonly referenceLinkStartPattern = /\[([^\]]*?)\]\[\s*([^\s\(\)]*)$/;
/// [id]: |
private readonly definitionPattern = /^\s*\[[\w\-]+\]:\s*([^\s]*)$/m;
private getPathCompletionContext(document: SkinnyTextDocument, position: vscode.Position): CompletionContext | undefined {
const line = document.lineAt(position.line).text;
const linePrefixText = line.slice(0, position.character);
const lineSuffixText = line.slice(position.character);
const linkPrefixMatch = linePrefixText.match(this.linkStartPattern);
if (linkPrefixMatch) {
const prefix = linkPrefixMatch[2];
if (this.refLooksLikeUrl(prefix)) {
return undefined;
}
const suffix = lineSuffixText.match(/^[^\)\s]*/);
return {
kind: CompletionContextKind.Link,
linkPrefix: tryDecodeUriComponent(prefix),
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
anchorInfo: this.getAnchorContext(prefix),
};
}
const definitionLinkPrefixMatch = linePrefixText.match(this.definitionPattern);
if (definitionLinkPrefixMatch) {
const prefix = definitionLinkPrefixMatch[1];
if (this.refLooksLikeUrl(prefix)) {
return undefined;
}
const suffix = lineSuffixText.match(/^[^\s]*/);
return {
kind: CompletionContextKind.LinkDefinition,
linkPrefix: tryDecodeUriComponent(prefix),
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
anchorInfo: this.getAnchorContext(prefix),
};
}
const referenceLinkPrefixMatch = linePrefixText.match(this.referenceLinkStartPattern);
if (referenceLinkPrefixMatch) {
const prefix = referenceLinkPrefixMatch[2];
const suffix = lineSuffixText.match(/^[^\]\s]*/);
return {
kind: CompletionContextKind.ReferenceLink,
linkPrefix: prefix,
linkTextStartPosition: position.translate({ characterDelta: -prefix.length }),
linkSuffix: suffix ? suffix[0] : '',
};
}
return undefined;
}
/**
* Check if {@param ref} looks like a 'http:' style url.
*/
private refLooksLikeUrl(prefix: string): boolean {
return /^\s*[\w\d\-]+:/.test(prefix);
}
private getAnchorContext(prefix: string): AnchorContext | undefined {
const anchorMatch = prefix.match(/^(.*)#([\w\d\-]*)$/);
if (!anchorMatch) {
return undefined;
}
return {
beforeAnchor: anchorMatch[1],
anchorPrefix: anchorMatch[2],
};
}
private async *provideReferenceSuggestions(document: SkinnyTextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {
const insertionRange = new vscode.Range(context.linkTextStartPosition, position);
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
const definitions = await this.linkProvider.getLinkDefinitions(document);
for (const def of definitions) {
yield {
kind: vscode.CompletionItemKind.Reference,
label: def.ref.text,
range: {
inserting: insertionRange,
replacing: replacementRange,
},
};
}
}
private async *provideHeaderSuggestions(document: SkinnyTextDocument, position: vscode.Position, context: CompletionContext, insertionRange: vscode.Range): AsyncIterable<vscode.CompletionItem> {
const toc = await TableOfContents.createForDocumentOrNotebook(this.engine, document);
for (const entry of toc.entries) {
const replacementRange = new vscode.Range(insertionRange.start, position.translate({ characterDelta: context.linkSuffix.length }));
yield {
kind: vscode.CompletionItemKind.Reference,
label: '#' + decodeURIComponent(entry.slug.value),
range: {
inserting: insertionRange,
replacing: replacementRange,
},
};
}
}
private async *providePathSuggestions(document: SkinnyTextDocument, position: vscode.Position, context: CompletionContext): AsyncIterable<vscode.CompletionItem> {
const valueBeforeLastSlash = context.linkPrefix.substring(0, context.linkPrefix.lastIndexOf('/') + 1); // keep the last slash
const parentDir = this.resolveReference(document, valueBeforeLastSlash || '.');
if (!parentDir) {
return;
}
const pathSegmentStart = position.translate({ characterDelta: valueBeforeLastSlash.length - context.linkPrefix.length });
const insertRange = new vscode.Range(pathSegmentStart, position);
const pathSegmentEnd = position.translate({ characterDelta: context.linkSuffix.length });
const replacementRange = new vscode.Range(pathSegmentStart, pathSegmentEnd);
let dirInfo: Array<[string, vscode.FileType]>;
try {
dirInfo = await vscode.workspace.fs.readDirectory(parentDir);
} catch {
return;
}
for (const [name, type] of dirInfo) {
// Exclude paths that start with `.`
if (name.startsWith('.')) {
continue;
}
const isDir = type === vscode.FileType.Directory;
yield {
label: isDir ? name + '/' : name,
insertText: isDir ? encodeURIComponent(name) + '/' : encodeURIComponent(name),
kind: isDir ? vscode.CompletionItemKind.Folder : vscode.CompletionItemKind.File,
range: {
inserting: insertRange,
replacing: replacementRange,
},
command: isDir ? { command: 'editor.action.triggerSuggest', title: '' } : undefined,
};
}
}
private resolveReference(document: SkinnyTextDocument, ref: string): vscode.Uri | undefined {
const docUri = this.getFileUriOfTextDocument(document);
if (ref.startsWith('/')) {
const workspaceFolder = vscode.workspace.getWorkspaceFolder(docUri);
if (workspaceFolder) {
return vscode.Uri.joinPath(workspaceFolder.uri, ref);
} else {
return this.resolvePath(docUri, ref.slice(1));
}
}
return this.resolvePath(docUri, ref);
}
private resolvePath(root: vscode.Uri, ref: string): vscode.Uri | undefined {
try {
if (root.scheme === 'file') {
return vscode.Uri.file(resolve(dirname(root.fsPath), ref));
} else {
return root.with({
path: resolve(dirname(root.path), ref),
});
}
} catch {
return undefined;
}
}
private getFileUriOfTextDocument(document: SkinnyTextDocument) {
if (document.uri.scheme === 'vscode-notebook-cell') {
const notebook = vscode.workspace.notebookDocuments
.find(notebook => notebook.getCells().some(cell => cell.document === document));
if (notebook) {
return notebook.uri;
}
}
return document.uri;
}
}

View File

@@ -1,308 +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 vscode from 'vscode';
import * as uri from 'vscode-uri';
import { MarkdownEngine } from '../markdownEngine';
import { Slugifier } from '../slugify';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { noopToken } from '../test/util';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref, MdLink, MdLinkProvider } from './documentLinkProvider';
import { MdWorkspaceCache } from './workspaceCache';
/**
* A link in a markdown file.
*/
export interface MdLinkReference {
readonly kind: 'link';
readonly isTriggerLocation: boolean;
readonly isDefinition: boolean;
readonly location: vscode.Location;
readonly link: MdLink;
}
/**
* A header in a markdown file.
*/
export interface MdHeaderReference {
readonly kind: 'header';
readonly isTriggerLocation: boolean;
readonly isDefinition: boolean;
/**
* The range of the header.
*
* In `# a b c #` this would be the range of `# a b c #`
*/
readonly location: vscode.Location;
/**
* The text of the header.
*
* In `# a b c #` this would be `a b c`
*/
readonly headerText: string;
/**
* The range of the header text itself.
*
* In `# a b c #` this would be the range of `a b c`
*/
readonly headerTextLocation: vscode.Location;
}
export type MdReference = MdLinkReference | MdHeaderReference;
export class MdReferencesProvider extends Disposable implements vscode.ReferenceProvider {
private readonly _linkCache: MdWorkspaceCache<readonly MdLink[]>;
public constructor(
private readonly linkProvider: MdLinkProvider,
private readonly workspaceContents: MdWorkspaceContents,
private readonly engine: MarkdownEngine,
private readonly slugifier: Slugifier,
) {
super();
this._linkCache = this._register(new MdWorkspaceCache(workspaceContents, doc => linkProvider.getAllLinks(doc, noopToken)));
}
async provideReferences(document: SkinnyTextDocument, position: vscode.Position, context: vscode.ReferenceContext, token: vscode.CancellationToken): Promise<vscode.Location[] | undefined> {
const allRefs = await this.getAllReferencesAtPosition(document, position, token);
return allRefs
.filter(ref => context.includeDeclaration || !ref.isDefinition)
.map(ref => ref.location);
}
public async getAllReferencesAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const toc = await TableOfContents.create(this.engine, document);
if (token.isCancellationRequested) {
return [];
}
const header = toc.entries.find(entry => entry.line === position.line);
if (header) {
return this.getReferencesToHeader(document, header);
} else {
return this.getReferencesToLinkAtPosition(document, position, token);
}
}
private async getReferencesToHeader(document: SkinnyTextDocument, header: TocEntry): Promise<MdReference[]> {
const links = (await this._linkCache.getAll()).flat();
const references: MdReference[] = [];
references.push({
kind: 'header',
isTriggerLocation: true,
isDefinition: true,
location: header.headerLocation,
headerText: header.text,
headerTextLocation: header.headerTextLocation
});
for (const link of links) {
if (link.href.kind === 'internal'
&& this.looksLikeLinkToDoc(link.href, document.uri)
&& this.slugifier.fromHeading(link.href.fragment).value === header.slug.value
) {
references.push({
kind: 'link',
isTriggerLocation: false,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
return references;
}
private async getReferencesToLinkAtPosition(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const docLinks = await this.linkProvider.getAllLinks(document, token);
for (const link of docLinks) {
if (link.kind === 'definition') {
// We could be in either the ref name or the definition
if (link.ref.range.contains(position)) {
return Array.from(this.getReferencesToLinkReference(docLinks, link.ref.text, { resource: document.uri, range: link.ref.range }));
} else if (link.source.hrefRange.contains(position)) {
return this.getReferencesToLink(link, position, token);
}
} else {
if (link.source.hrefRange.contains(position)) {
return this.getReferencesToLink(link, position, token);
}
}
}
return [];
}
private async getReferencesToLink(sourceLink: MdLink, triggerPosition: vscode.Position, token: vscode.CancellationToken): Promise<MdReference[]> {
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
if (token.isCancellationRequested) {
return [];
}
if (sourceLink.href.kind === 'reference') {
return Array.from(this.getReferencesToLinkReference(allLinksInWorkspace, sourceLink.href.ref, { resource: sourceLink.source.resource, range: sourceLink.source.hrefRange }));
}
if (sourceLink.href.kind === 'external') {
const references: MdReference[] = [];
for (const link of allLinksInWorkspace) {
if (link.href.kind === 'external' && link.href.uri.toString() === sourceLink.href.uri.toString()) {
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
return references;
}
const targetDoc = await tryFindMdDocumentForLink(sourceLink.href, this.workspaceContents);
if (token.isCancellationRequested) {
return [];
}
const references: MdReference[] = [];
if (targetDoc && sourceLink.href.fragment && sourceLink.source.fragmentRange?.contains(triggerPosition)) {
const toc = await TableOfContents.create(this.engine, targetDoc);
const entry = toc.lookup(sourceLink.href.fragment);
if (entry) {
references.push({
kind: 'header',
isTriggerLocation: false,
isDefinition: true,
location: entry.headerLocation,
headerText: entry.text,
headerTextLocation: entry.headerTextLocation
});
}
for (const link of allLinksInWorkspace) {
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, targetDoc.uri)) {
continue;
}
if (this.slugifier.fromHeading(link.href.fragment).equals(this.slugifier.fromHeading(sourceLink.href.fragment))) {
const isTriggerLocation = sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
references.push({
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, link.source.hrefRange),
});
}
}
} else { // Triggered on a link without a fragment so we only require matching the file and ignore fragments
references.push(...this.findAllLinksToFile(targetDoc?.uri ?? sourceLink.href.path, allLinksInWorkspace, sourceLink));
}
return references;
}
private looksLikeLinkToDoc(href: InternalHref, targetDoc: vscode.Uri) {
return href.path.fsPath === targetDoc.fsPath
|| uri.Utils.extname(href.path) === '' && href.path.with({ path: href.path.path + '.md' }).fsPath === targetDoc.fsPath;
}
public async getAllReferencesToFile(resource: vscode.Uri, _token: vscode.CancellationToken): Promise<MdReference[]> {
const allLinksInWorkspace = (await this._linkCache.getAll()).flat();
return Array.from(this.findAllLinksToFile(resource, allLinksInWorkspace, undefined));
}
private * findAllLinksToFile(resource: vscode.Uri, allLinksInWorkspace: readonly MdLink[], sourceLink: MdLink | undefined): Iterable<MdReference> {
for (const link of allLinksInWorkspace) {
if (link.href.kind !== 'internal' || !this.looksLikeLinkToDoc(link.href, resource)) {
continue;
}
// Exclude cases where the file is implicitly referencing itself
if (link.source.text.startsWith('#') && link.source.resource.fsPath === resource.fsPath) {
continue;
}
const isTriggerLocation = !!sourceLink && sourceLink.source.resource.fsPath === link.source.resource.fsPath && sourceLink.source.hrefRange.isEqual(link.source.hrefRange);
const pathRange = this.getPathRange(link);
yield {
kind: 'link',
isTriggerLocation,
isDefinition: false,
link,
location: new vscode.Location(link.source.resource, pathRange),
};
}
}
private * getReferencesToLinkReference(allLinks: Iterable<MdLink>, refToFind: string, from: { resource: vscode.Uri; range: vscode.Range }): Iterable<MdReference> {
for (const link of allLinks) {
let ref: string;
if (link.kind === 'definition') {
ref = link.ref.text;
} else if (link.href.kind === 'reference') {
ref = link.href.ref;
} else {
continue;
}
if (ref === refToFind && link.source.resource.fsPath === from.resource.fsPath) {
const isTriggerLocation = from.resource.fsPath === link.source.resource.fsPath && (
(link.href.kind === 'reference' && from.range.isEqual(link.source.hrefRange)) || (link.kind === 'definition' && from.range.isEqual(link.ref.range)));
const pathRange = this.getPathRange(link);
yield {
kind: 'link',
isTriggerLocation,
isDefinition: link.kind === 'definition',
link,
location: new vscode.Location(from.resource, pathRange),
};
}
}
}
/**
* Get just the range of the file path, dropping the fragment
*/
private getPathRange(link: MdLink): vscode.Range {
return link.source.fragmentRange
? link.source.hrefRange.with(undefined, link.source.fragmentRange.start.translate(0, -1))
: link.source.hrefRange;
}
}
export async function tryFindMdDocumentForLink(href: InternalHref, workspaceContents: MdWorkspaceContents): Promise<SkinnyTextDocument | undefined> {
const targetDoc = await workspaceContents.getMarkdownDocument(href.path);
if (targetDoc) {
return targetDoc;
}
// We don't think the file exists. If it doesn't already have an extension, try tacking on a `.md` and using that instead
if (uri.Utils.extname(href.path) === '') {
const dotMdResource = href.path.with({ path: href.path.path + '.md' });
return workspaceContents.getMarkdownDocument(dotMdResource);
}
return undefined;
}

View File

@@ -1,272 +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 path from 'path';
import * as vscode from 'vscode';
import * as nls from 'vscode-nls';
import * as URI from 'vscode-uri';
import { Slugifier } from '../slugify';
import { Disposable } from '../util/dispose';
import { resolveDocumentLink } from '../util/openDocumentLink';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
import { InternalHref } from './documentLinkProvider';
import { MdHeaderReference, MdLinkReference, MdReference, MdReferencesProvider, tryFindMdDocumentForLink } from './references';
const localize = nls.loadMessageBundle();
export interface MdReferencesResponse {
references: MdReference[];
triggerRef: MdReference;
}
interface MdFileRenameEdit {
readonly from: vscode.Uri;
readonly to: vscode.Uri;
}
/**
* Type with additional metadata about the edits for testing
*
* This is needed since `vscode.WorkspaceEdit` does not expose info on file renames.
*/
export interface MdWorkspaceEdit {
readonly edit: vscode.WorkspaceEdit;
readonly fileRenames?: ReadonlyArray<MdFileRenameEdit>;
}
function tryDecodeUri(str: string): string {
try {
return decodeURI(str);
} catch {
return str;
}
}
export class MdRenameProvider extends Disposable implements vscode.RenameProvider {
private cachedRefs?: {
readonly resource: vscode.Uri;
readonly version: number;
readonly position: vscode.Position;
readonly triggerRef: MdReference;
readonly references: MdReference[];
} | undefined;
private readonly renameNotSupportedText = localize('invalidRenameLocation', "Rename not supported at location");
public constructor(
private readonly referencesProvider: MdReferencesProvider,
private readonly workspaceContents: MdWorkspaceContents,
private readonly slugifier: Slugifier,
) {
super();
}
public async prepareRename(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<undefined | { readonly range: vscode.Range; readonly placeholder: string }> {
const allRefsInfo = await this.getAllReferences(document, position, token);
if (token.isCancellationRequested) {
return undefined;
}
if (!allRefsInfo || !allRefsInfo.references.length) {
throw new Error(this.renameNotSupportedText);
}
const triggerRef = allRefsInfo.triggerRef;
switch (triggerRef.kind) {
case 'header': {
return { range: triggerRef.headerTextLocation.range, placeholder: triggerRef.headerText };
}
case 'link': {
if (triggerRef.link.kind === 'definition') {
// We may have been triggered on the ref or the definition itself
if (triggerRef.link.ref.range.contains(position)) {
return { range: triggerRef.link.ref.range, placeholder: triggerRef.link.ref.text };
}
}
if (triggerRef.link.href.kind === 'external') {
return { range: triggerRef.link.source.hrefRange, placeholder: document.getText(triggerRef.link.source.hrefRange) };
}
// See if we are renaming the fragment or the path
const { fragmentRange } = triggerRef.link.source;
if (fragmentRange?.contains(position)) {
const declaration = this.findHeaderDeclaration(allRefsInfo.references);
if (declaration) {
return { range: fragmentRange, placeholder: declaration.headerText };
}
return { range: fragmentRange, placeholder: document.getText(fragmentRange) };
}
const range = this.getFilePathRange(triggerRef);
if (!range) {
throw new Error(this.renameNotSupportedText);
}
return { range, placeholder: tryDecodeUri(document.getText(range)) };
}
}
}
private getFilePathRange(ref: MdLinkReference): vscode.Range {
if (ref.link.source.fragmentRange) {
return ref.link.source.hrefRange.with(undefined, ref.link.source.fragmentRange.start.translate(0, -1));
}
return ref.link.source.hrefRange;
}
private findHeaderDeclaration(references: readonly MdReference[]): MdHeaderReference | undefined {
return references.find(ref => ref.isDefinition && ref.kind === 'header') as MdHeaderReference | undefined;
}
public async provideRenameEdits(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<vscode.WorkspaceEdit | undefined> {
return (await this.provideRenameEditsImpl(document, position, newName, token))?.edit;
}
public async provideRenameEditsImpl(document: SkinnyTextDocument, position: vscode.Position, newName: string, token: vscode.CancellationToken): Promise<MdWorkspaceEdit | undefined> {
const allRefsInfo = await this.getAllReferences(document, position, token);
if (token.isCancellationRequested || !allRefsInfo || !allRefsInfo.references.length) {
return undefined;
}
const triggerRef = allRefsInfo.triggerRef;
if (triggerRef.kind === 'link' && (
(triggerRef.link.kind === 'definition' && triggerRef.link.ref.range.contains(position)) || triggerRef.link.href.kind === 'reference'
)) {
return this.renameReferenceLinks(allRefsInfo, newName);
} else if (triggerRef.kind === 'link' && triggerRef.link.href.kind === 'external') {
return this.renameExternalLink(allRefsInfo, newName);
} else if (triggerRef.kind === 'header' || (triggerRef.kind === 'link' && triggerRef.link.source.fragmentRange?.contains(position) && (triggerRef.link.kind === 'definition' || triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal'))) {
return this.renameFragment(allRefsInfo, newName);
} else if (triggerRef.kind === 'link' && !triggerRef.link.source.fragmentRange?.contains(position) && triggerRef.link.kind === 'link' && triggerRef.link.href.kind === 'internal') {
return this.renameFilePath(triggerRef.link.source.resource, triggerRef.link.href, allRefsInfo, newName);
}
return undefined;
}
private async renameFilePath(triggerDocument: vscode.Uri, triggerHref: InternalHref, allRefsInfo: MdReferencesResponse, newName: string): Promise<MdWorkspaceEdit> {
const edit = new vscode.WorkspaceEdit();
const fileRenames: MdFileRenameEdit[] = [];
const targetDoc = await tryFindMdDocumentForLink(triggerHref, this.workspaceContents);
const targetUri = targetDoc?.uri ?? triggerHref.path;
const rawNewFilePath = resolveDocumentLink(newName, triggerDocument);
let resolvedNewFilePath = rawNewFilePath;
if (!URI.Utils.extname(resolvedNewFilePath)) {
// If the newly entered path doesn't have a file extension but the original file did
// tack on a .md file extension
if (URI.Utils.extname(targetUri)) {
resolvedNewFilePath = resolvedNewFilePath.with({
path: resolvedNewFilePath.path + '.md'
});
}
}
// First rename the file
if (await this.workspaceContents.pathExists(targetUri)) {
fileRenames.push({ from: targetUri, to: resolvedNewFilePath });
edit.renameFile(targetUri, resolvedNewFilePath);
}
// Then update all refs to it
for (const ref of allRefsInfo.references) {
if (ref.kind === 'link') {
// Try to preserve style of existing links
let newPath: string;
if (ref.link.source.text.startsWith('/')) {
const root = resolveDocumentLink('/', ref.link.source.resource);
newPath = '/' + path.relative(root.toString(true), rawNewFilePath.toString(true));
} else {
const rootDir = URI.Utils.dirname(ref.link.source.resource);
if (rootDir.scheme === rawNewFilePath.scheme && rootDir.scheme !== 'untitled') {
newPath = path.relative(rootDir.toString(true), rawNewFilePath.toString(true));
if (newName.startsWith('./') && !newPath.startsWith('../') || newName.startsWith('.\\') && !newPath.startsWith('..\\')) {
newPath = './' + newPath;
}
} else {
newPath = newName;
}
}
edit.replace(ref.link.source.resource, this.getFilePathRange(ref), encodeURI(newPath.replace(/\\/g, '/')));
}
}
return { edit, fileRenames };
}
private renameFragment(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const slug = this.slugifier.fromHeading(newName).value;
const edit = new vscode.WorkspaceEdit();
for (const ref of allRefsInfo.references) {
switch (ref.kind) {
case 'header':
edit.replace(ref.location.uri, ref.headerTextLocation.range, newName);
break;
case 'link':
edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, !ref.link.source.fragmentRange || ref.link.href.kind === 'external' ? newName : slug);
break;
}
}
return { edit };
}
private renameExternalLink(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const edit = new vscode.WorkspaceEdit();
for (const ref of allRefsInfo.references) {
if (ref.kind === 'link') {
edit.replace(ref.link.source.resource, ref.location.range, newName);
}
}
return { edit };
}
private renameReferenceLinks(allRefsInfo: MdReferencesResponse, newName: string): MdWorkspaceEdit {
const edit = new vscode.WorkspaceEdit();
for (const ref of allRefsInfo.references) {
if (ref.kind === 'link') {
if (ref.link.kind === 'definition') {
edit.replace(ref.link.source.resource, ref.link.ref.range, newName);
} else {
edit.replace(ref.link.source.resource, ref.link.source.fragmentRange ?? ref.location.range, newName);
}
}
}
return { edit };
}
private async getAllReferences(document: SkinnyTextDocument, position: vscode.Position, token: vscode.CancellationToken): Promise<MdReferencesResponse | undefined> {
const version = document.version;
if (this.cachedRefs
&& this.cachedRefs.resource.fsPath === document.uri.fsPath
&& this.cachedRefs.version === document.version
&& this.cachedRefs.position.isEqual(position)
) {
return this.cachedRefs;
}
const references = await this.referencesProvider.getAllReferencesAtPosition(document, position, token);
const triggerRef = references.find(ref => ref.isTriggerLocation);
if (!triggerRef) {
return undefined;
}
this.cachedRefs = {
resource: document.uri,
version,
position,
references,
triggerRef
};
return this.cachedRefs;
}
}

View File

@@ -1,251 +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 Token = require('markdown-it/lib/token');
import * as vscode from 'vscode';
import { MarkdownEngine } from '../markdownEngine';
import { TableOfContents, TocEntry } from '../tableOfContents';
import { SkinnyTextDocument } from '../workspaceContents';
interface MarkdownItTokenWithMap extends Token {
map: [number, number];
}
export class MdSmartSelect implements vscode.SelectionRangeProvider {
constructor(
private readonly engine: MarkdownEngine
) { }
public async provideSelectionRanges(document: SkinnyTextDocument, positions: vscode.Position[], _token: vscode.CancellationToken): Promise<vscode.SelectionRange[] | undefined> {
const promises = await Promise.all(positions.map((position) => {
return this.provideSelectionRange(document, position, _token);
}));
return promises.filter(item => item !== undefined) as vscode.SelectionRange[];
}
private async provideSelectionRange(document: SkinnyTextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise<vscode.SelectionRange | undefined> {
const headerRange = await this.getHeaderSelectionRange(document, position);
const blockRange = await this.getBlockSelectionRange(document, position, headerRange);
const inlineRange = await this.getInlineSelectionRange(document, position, blockRange);
return inlineRange || blockRange || headerRange;
}
private async getInlineSelectionRange(document: SkinnyTextDocument, position: vscode.Position, blockRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
return createInlineRange(document, position, blockRange);
}
private async getBlockSelectionRange(document: SkinnyTextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
const tokens = await this.engine.parse(document);
const blockTokens = getBlockTokensForPosition(tokens, position, headerRange);
if (blockTokens.length === 0) {
return undefined;
}
let currentRange: vscode.SelectionRange | undefined = headerRange ? headerRange : createBlockRange(blockTokens.shift()!, document, position.line);
for (let i = 0; i < blockTokens.length; i++) {
currentRange = createBlockRange(blockTokens[i], document, position.line, currentRange);
}
return currentRange;
}
private async getHeaderSelectionRange(document: SkinnyTextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
const toc = await TableOfContents.create(this.engine, document);
const headerInfo = getHeadersForPosition(toc.entries, position);
const headers = headerInfo.headers;
let currentRange: vscode.SelectionRange | undefined;
for (let i = 0; i < headers.length; i++) {
currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc.entries));
}
return currentRange;
}
}
function getHeadersForPosition(toc: readonly TocEntry[], position: vscode.Position): { headers: TocEntry[]; headerOnThisLine: boolean } {
const enclosingHeaders = toc.filter(header => header.sectionLocation.range.start.line <= position.line && header.sectionLocation.range.end.line >= position.line);
const sortedHeaders = enclosingHeaders.sort((header1, header2) => (header1.line - position.line) - (header2.line - position.line));
const onThisLine = toc.find(header => header.line === position.line) !== undefined;
return {
headers: sortedHeaders,
headerOnThisLine: onThisLine
};
}
function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, onHeaderLine: boolean, parent?: vscode.SelectionRange, startOfChildRange?: vscode.Position): vscode.SelectionRange | undefined {
const range = header.sectionLocation.range;
const contentRange = new vscode.Range(range.start.translate(1), range.end);
if (onHeaderLine && isClosestHeaderToPosition && startOfChildRange) {
// selection was made on this header line, so select header and its content until the start of its first child
// then all of its content
return new vscode.SelectionRange(range.with(undefined, startOfChildRange), new vscode.SelectionRange(range, parent));
} else if (onHeaderLine && isClosestHeaderToPosition) {
// selection was made on this header line and no children so expand to all of its content
return new vscode.SelectionRange(range, parent);
} else if (isClosestHeaderToPosition && startOfChildRange) {
// selection was made within content and has child so select content
// of this header then all content then header
return new vscode.SelectionRange(contentRange.with(undefined, startOfChildRange), new vscode.SelectionRange(contentRange, (new vscode.SelectionRange(range, parent))));
} else {
// not on this header line so select content then header
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent));
}
}
function getBlockTokensForPosition(tokens: Token[], position: vscode.Position, parent?: vscode.SelectionRange): MarkdownItTokenWithMap[] {
const enclosingTokens = tokens.filter((token): token is MarkdownItTokenWithMap => !!token.map && (token.map[0] <= position.line && token.map[1] > position.line) && (!parent || (token.map[0] >= parent.range.start.line && token.map[1] <= parent.range.end.line + 1)) && isBlockElement(token));
if (enclosingTokens.length === 0) {
return [];
}
const sortedTokens = enclosingTokens.sort((token1, token2) => (token2.map[1] - token2.map[0]) - (token1.map[1] - token1.map[0]));
return sortedTokens;
}
function createBlockRange(block: MarkdownItTokenWithMap, document: SkinnyTextDocument, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
if (block.type === 'fence') {
return createFencedRange(block, cursorLine, document, parent);
} else {
let startLine = document.lineAt(block.map[0]).isEmptyOrWhitespace ? block.map[0] + 1 : block.map[0];
let endLine = startLine === block.map[1] ? block.map[1] : block.map[1] - 1;
if (block.type === 'paragraph_open' && block.map[1] - block.map[0] === 2) {
startLine = endLine = cursorLine;
} else if (isList(block) && document.lineAt(endLine).isEmptyOrWhitespace) {
endLine = endLine - 1;
}
const range = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text?.length ?? 0);
if (parent?.range.contains(range) && !parent.range.isEqual(range)) {
return new vscode.SelectionRange(range, parent);
} else if (parent?.range.isEqual(range)) {
return parent;
} else {
return new vscode.SelectionRange(range);
}
}
}
function createInlineRange(document: SkinnyTextDocument, cursorPosition: vscode.Position, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
const lineText = document.lineAt(cursorPosition.line).text;
const boldSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, parent);
const italicSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, parent);
let comboSelection: vscode.SelectionRange | undefined;
if (boldSelection && italicSelection && !boldSelection.range.isEqual(italicSelection.range)) {
if (boldSelection.range.contains(italicSelection.range)) {
comboSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, true, boldSelection);
} else if (italicSelection.range.contains(boldSelection.range)) {
comboSelection = createBoldRange(lineText, cursorPosition.character, cursorPosition.line, italicSelection);
}
}
const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, comboSelection || boldSelection || italicSelection || parent);
const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection || parent);
return inlineCodeBlockSelection || linkSelection || comboSelection || boldSelection || italicSelection;
}
function createFencedRange(token: MarkdownItTokenWithMap, cursorLine: number, document: SkinnyTextDocument, parent?: vscode.SelectionRange): vscode.SelectionRange {
const startLine = token.map[0];
const endLine = token.map[1] - 1;
const onFenceLine = cursorLine === startLine || cursorLine === endLine;
const fenceRange = new vscode.Range(startLine, 0, endLine, document.lineAt(endLine).text.length);
const contentRange = endLine - startLine > 2 && !onFenceLine ? new vscode.Range(startLine + 1, 0, endLine - 1, document.lineAt(endLine - 1).text.length) : undefined;
if (contentRange) {
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(fenceRange, parent));
} else {
if (parent?.range.isEqual(fenceRange)) {
return parent;
} else {
return new vscode.SelectionRange(fenceRange, parent);
}
}
}
function createBoldRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
const regex = /\*\*([^*]+\*?[^*]+\*?[^*]+)\*\*/gim;
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
if (matches.length) {
// should only be one match, so select first and index 0 contains the entire match
const bold = matches[0][0];
const startIndex = lineText.indexOf(bold);
const cursorOnStars = cursorChar === startIndex || cursorChar === startIndex + 1 || cursorChar === startIndex + bold.length || cursorChar === startIndex + bold.length - 1;
const contentAndStars = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + bold.length), parent);
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 2, cursorLine, startIndex + bold.length - 2), contentAndStars);
return cursorOnStars ? contentAndStars : content;
}
return undefined;
}
function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine: number, isItalic: boolean, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
const italicRegexes = [/(?:[^*]+)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]+)/g, /^(?:[^*]*)(\*([^*]+)(?:\*\*[^*]*\*\*)*([^*]+)\*)(?:[^*]*)$/g];
let matches = [];
if (isItalic) {
matches = [...lineText.matchAll(italicRegexes[0])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
if (!matches.length) {
matches = [...lineText.matchAll(italicRegexes[1])].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
}
} else {
matches = [...lineText.matchAll(/\`[^\`]*\`/g)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length >= cursorChar);
}
if (matches.length) {
// should only be one match, so select first and select group 1 for italics because that contains just the italic section
// doesn't include the leading and trailing characters which are guaranteed to not be * so as not to be confused with bold
const match = isItalic ? matches[0][1] : matches[0][0];
const startIndex = lineText.indexOf(match);
const cursorOnType = cursorChar === startIndex || cursorChar === startIndex + match.length;
const contentAndType = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex, cursorLine, startIndex + match.length), parent);
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, startIndex + 1, cursorLine, startIndex + match.length - 1), contentAndType);
return cursorOnType ? contentAndType : content;
}
return undefined;
}
function createLinkRange(lineText: string, cursorChar: number, cursorLine: number, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
const regex = /(\[[^\(\)]*\])(\([^\[\]]*\))/g;
const matches = [...lineText.matchAll(regex)].filter(match => lineText.indexOf(match[0]) <= cursorChar && lineText.indexOf(match[0]) + match[0].length > cursorChar);
if (matches.length) {
// should only be one match, so select first and index 0 contains the entire match, so match = [text](url)
const link = matches[0][0];
const linkRange = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(link), cursorLine, lineText.indexOf(link) + link.length), parent);
const linkText = matches[0][1];
const url = matches[0][2];
// determine if cursor is within [text] or (url) in order to know which should be selected
const nearestType = cursorChar >= lineText.indexOf(linkText) && cursorChar < lineText.indexOf(linkText) + linkText.length ? linkText : url;
const indexOfType = lineText.indexOf(nearestType);
// determine if cursor is on a bracket or paren and if so, return the [content] or (content), skipping over the content range
const cursorOnType = cursorChar === indexOfType || cursorChar === indexOfType + nearestType.length;
const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType, cursorLine, indexOfType + nearestType.length), linkRange);
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, indexOfType + 1, cursorLine, indexOfType + nearestType.length - 1), contentAndNearestType);
return cursorOnType ? contentAndNearestType : content;
}
return undefined;
}
function isList(token: Token): boolean {
return token.type ? ['ordered_list_open', 'list_item_open', 'bullet_list_open'].includes(token.type) : false;
}
function isBlockElement(token: Token): boolean {
return !['list_item_close', 'paragraph_close', 'bullet_list_close', 'inline', 'heading_close', 'heading_open'].includes(token.type);
}
function getFirstChildHeader(document: SkinnyTextDocument, header?: TocEntry, toc?: readonly TocEntry[]): vscode.Position | undefined {
let childRange: vscode.Position | undefined;
if (header && toc) {
let children = toc.filter(t => header.sectionLocation.range.contains(t.sectionLocation.range) && t.sectionLocation.range.start.line > header.sectionLocation.range.start.line).sort((t1, t2) => t1.line - t2.line);
if (children.length > 0) {
childRange = children[0].sectionLocation.range.start;
const lineText = document.lineAt(childRange.line - 1).text;
return childRange ? childRange.translate(-1, lineText.length) : undefined;
}
}
return undefined;
}

View File

@@ -1,61 +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 vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { Lazy, lazy } from '../util/lazy';
import { MdWorkspaceContents, SkinnyTextDocument } from '../workspaceContents';
/**
* Cache of information for markdown files in the workspace.
*/
export class MdWorkspaceCache<T> extends Disposable {
private readonly _cache = new Map<string, Lazy<Promise<T>>>();
private _hasPopulatedCache = false;
public constructor(
private readonly workspaceContents: MdWorkspaceContents,
private readonly getValue: (document: SkinnyTextDocument) => Promise<T>,
) {
super();
}
public async getAll(): Promise<T[]> {
if (!this._hasPopulatedCache) {
await this.populateCache();
this._hasPopulatedCache = true;
this.workspaceContents.onDidChangeMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
this.workspaceContents.onDidCreateMarkdownDocument(this.onDidChangeDocument, this, this._disposables);
this.workspaceContents.onDidDeleteMarkdownDocument(this.onDidDeleteDocument, this, this._disposables);
}
return Promise.all(Array.from(this._cache.values(), x => x.value));
}
private async populateCache(): Promise<void> {
const markdownDocumentUris = await this.workspaceContents.getAllMarkdownDocuments();
for (const document of markdownDocumentUris) {
this.update(document);
}
}
private key(resource: vscode.Uri): string {
return resource.toString();
}
private update(document: SkinnyTextDocument): void {
this._cache.set(this.key(document.uri), lazy(() => this.getValue(document)));
}
private onDidChangeDocument(document: SkinnyTextDocument) {
this.update(document);
}
private onDidDeleteDocument(resource: vscode.Uri) {
this._cache.delete(this.key(resource));
}
}

View File

@@ -1,29 +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 vscode from 'vscode';
import { Disposable } from '../util/dispose';
import { MdWorkspaceContents } from '../workspaceContents';
import { MdDocumentSymbolProvider } from './documentSymbolProvider';
import { MdWorkspaceCache } from './workspaceCache';
export class MdWorkspaceSymbolProvider extends Disposable implements vscode.WorkspaceSymbolProvider {
private readonly _cache: MdWorkspaceCache<vscode.SymbolInformation[]>;
public constructor(
symbolProvider: MdDocumentSymbolProvider,
workspaceContents: MdWorkspaceContents,
) {
super();
this._cache = this._register(new MdWorkspaceCache(workspaceContents, doc => symbolProvider.provideDocumentSymbolInformation(doc)));
}
public async provideWorkspaceSymbols(query: string): Promise<vscode.SymbolInformation[]> {
const allSymbols = (await this._cache.getAll()).flat();
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
}
}