mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-21 09:35:38 -05:00
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:
@@ -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']
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) ? '`);
|
||||
|
||||
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) ? '`);
|
||||
|
||||
if (i <= uris.length - 1 && uris.length > 1) {
|
||||
snippet.appendText(' ');
|
||||
}
|
||||
});
|
||||
|
||||
return snippet;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user