Files
azuredatastudio/extensions/markdown-language-features/src/languageFeatures/pathCompletions.ts
Karl Burtram 8a3d08f0de Merge vscode 1.67 (#20883)
* Fix initial build breaks from 1.67 merge (#2514)

* Update yarn lock files

* Update build scripts

* Fix tsconfig

* Build breaks

* WIP

* Update yarn lock files

* Misc breaks

* Updates to package.json

* Breaks

* Update yarn

* Fix breaks

* Breaks

* Build breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Missing file

* Breaks

* Breaks

* Breaks

* Breaks

* Breaks

* Fix several runtime breaks (#2515)

* Missing files

* Runtime breaks

* Fix proxy ordering issue

* Remove commented code

* Fix breaks with opening query editor

* Fix post merge break

* Updates related to setup build and other breaks (#2516)

* Fix bundle build issues

* Update distro

* Fix distro merge and update build JS files

* Disable pipeline steps

* Remove stats call

* Update license name

* Make new RPM dependencies a warning

* Fix extension manager version checks

* Update JS file

* Fix a few runtime breaks

* Fixes

* Fix runtime issues

* Fix build breaks

* Update notebook tests (part 1)

* Fix broken tests

* Linting errors

* Fix hygiene

* Disable lint rules

* Bump distro

* Turn off smoke tests

* Disable integration tests

* Remove failing "activate" test

* Remove failed test assertion

* Disable other broken test

* Disable query history tests

* Disable extension unit tests

* Disable failing tasks
2022-10-19 19:13:18 -07:00

354 lines
11 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* 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;
}
}