mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-17 02:51:36 -05:00
Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 (#14050)
* Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 * Fix breaks * Extension management fixes * Fix breaks in windows bundling * Fix/skip failing tests * Update distro * Add clear to nuget.config * Add hygiene task * Bump distro * Fix hygiene issue * Add build to hygiene exclusion * Update distro * Update hygiene * Hygiene exclusions * Update tsconfig * Bump distro for server breaks * Update build config * Update darwin path * Add done calls to notebook tests * Skip failing tests * Disable smoke tests
This commit is contained in:
@@ -154,15 +154,13 @@ table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table > thead > tr > th {
|
||||
th {
|
||||
text-align: left;
|
||||
border-bottom: 1px solid;
|
||||
}
|
||||
|
||||
table > thead > tr > th,
|
||||
table > thead > tr > td,
|
||||
table > tbody > tr > th,
|
||||
table > tbody > tr > td {
|
||||
th,
|
||||
td {
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
@@ -217,22 +215,22 @@ pre code {
|
||||
border-color: rgb(0, 0, 0);
|
||||
}
|
||||
|
||||
.vscode-light table > thead > tr > th {
|
||||
.vscode-light th {
|
||||
border-color: rgba(0, 0, 0, 0.69);
|
||||
}
|
||||
|
||||
.vscode-dark table > thead > tr > th {
|
||||
.vscode-dark th {
|
||||
border-color: rgba(255, 255, 255, 0.69);
|
||||
}
|
||||
|
||||
.vscode-light h1,
|
||||
.vscode-light hr,
|
||||
.vscode-light table > tbody > tr + tr > td {
|
||||
.vscode-light td {
|
||||
border-color: rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.vscode-dark h1,
|
||||
.vscode-dark hr,
|
||||
.vscode-dark table > tbody > tr + tr > td {
|
||||
.vscode-dark td {
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
@@ -13,10 +13,18 @@ import { isMarkdownFile } from '../util/file';
|
||||
import { isString } from 'util'; // {{ SQL CARBON EDIT }}
|
||||
|
||||
|
||||
type UriComponents = {
|
||||
readonly scheme?: string;
|
||||
readonly path: string;
|
||||
readonly fragment?: string;
|
||||
readonly authority?: string;
|
||||
readonly query?: string;
|
||||
};
|
||||
|
||||
export interface OpenDocumentLinkArgs {
|
||||
readonly path: {};
|
||||
readonly parts: UriComponents;
|
||||
readonly fragment: string;
|
||||
readonly fromResource: {};
|
||||
readonly fromResource: UriComponents;
|
||||
}
|
||||
|
||||
enum OpenMarkdownLinks {
|
||||
@@ -33,7 +41,7 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
path: vscode.Uri,
|
||||
fragment: string,
|
||||
): vscode.Uri {
|
||||
const toJson = (uri: vscode.Uri) => {
|
||||
const toJson = (uri: vscode.Uri): UriComponents => {
|
||||
return {
|
||||
scheme: uri.scheme,
|
||||
authority: uri.authority,
|
||||
@@ -43,7 +51,7 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
};
|
||||
};
|
||||
return vscode.Uri.parse(`command:${OpenDocumentLinkCommand.id}?${encodeURIComponent(JSON.stringify(<OpenDocumentLinkArgs>{
|
||||
path: toJson(path),
|
||||
parts: toJson(path),
|
||||
fragment,
|
||||
fromResource: toJson(fromResource),
|
||||
}))}`);
|
||||
@@ -57,36 +65,58 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
return OpenDocumentLinkCommand.execute(this.engine, args);
|
||||
}
|
||||
|
||||
public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs) {
|
||||
public static async execute(engine: MarkdownEngine, args: OpenDocumentLinkArgs): Promise<void> {
|
||||
const fromResource = vscode.Uri.parse('').with(args.fromResource);
|
||||
const targetResource = fromResource.scheme === 'file' && isString(args.path) ? vscode.Uri.file(args.path) : vscode.Uri.parse('').with(args.path); // {{ SQL CARBON EDIT }} Fix markdown relative links https://github.com/microsoft/azuredatastudio/issues/11657
|
||||
|
||||
const targetResource = reviveUri(args.parts);
|
||||
|
||||
const column = this.getViewColumn(fromResource);
|
||||
try {
|
||||
return await this.tryOpen(engine, targetResource, args, column);
|
||||
} catch {
|
||||
if (extname(targetResource.path) === '') {
|
||||
return this.tryOpen(engine, targetResource.with({ path: targetResource.path + '.md' }), args, column);
|
||||
}
|
||||
await vscode.commands.executeCommand('vscode.open', targetResource, column);
|
||||
return undefined;
|
||||
|
||||
const didOpen = await this.tryOpen(engine, targetResource, args, column);
|
||||
if (didOpen) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (extname(targetResource.path) === '') {
|
||||
await this.tryOpen(engine, targetResource.with({ path: targetResource.path + '.md' }), args, column);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static async tryOpen(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn) {
|
||||
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
|
||||
if (vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
|
||||
return this.tryRevealLine(engine, vscode.window.activeTextEditor, args.fragment);
|
||||
private static async tryOpen(engine: MarkdownEngine, resource: vscode.Uri, args: OpenDocumentLinkArgs, column: vscode.ViewColumn): Promise<boolean> {
|
||||
const tryUpdateForActiveFile = async (): Promise<boolean> => {
|
||||
if (vscode.window.activeTextEditor && isMarkdownFile(vscode.window.activeTextEditor.document)) {
|
||||
if (vscode.window.activeTextEditor.document.uri.fsPath === resource.fsPath) {
|
||||
await this.tryRevealLine(engine, vscode.window.activeTextEditor, args.fragment);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (await tryUpdateForActiveFile()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let stat: vscode.FileStat;
|
||||
try {
|
||||
stat = await vscode.workspace.fs.stat(resource);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stat = await vscode.workspace.fs.stat(resource);
|
||||
if (stat.type === vscode.FileType.Directory) {
|
||||
return vscode.commands.executeCommand('revealInExplorer', resource);
|
||||
await vscode.commands.executeCommand('revealInExplorer', resource);
|
||||
return true;
|
||||
}
|
||||
|
||||
return vscode.workspace.openTextDocument(resource)
|
||||
.then(document => vscode.window.showTextDocument(document, column))
|
||||
.then(editor => this.tryRevealLine(engine, editor, args.fragment));
|
||||
try {
|
||||
await vscode.commands.executeCommand('vscode.open', resource, column);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
return tryUpdateForActiveFile();
|
||||
}
|
||||
|
||||
private static getViewColumn(resource: vscode.Uri): vscode.ViewColumn {
|
||||
@@ -102,7 +132,7 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
}
|
||||
|
||||
private static async tryRevealLine(engine: MarkdownEngine, editor: vscode.TextEditor, fragment?: string) {
|
||||
if (editor && fragment) {
|
||||
if (fragment) {
|
||||
const toc = new TableOfContentsProvider(engine, editor.document);
|
||||
const entry = await toc.lookup(fragment);
|
||||
if (entry) {
|
||||
@@ -123,6 +153,12 @@ export class OpenDocumentLinkCommand implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
function reviveUri(parts: any) {
|
||||
if (parts.scheme === 'file' && isString(parts.path)) { // {{ SQL CARBON EDIT }} Fix markdown relative links https://github.com/microsoft/azuredatastudio/issues/11657
|
||||
return vscode.Uri.file(parts.path);
|
||||
}
|
||||
return vscode.Uri.parse('').with(parts);
|
||||
}
|
||||
|
||||
export async function resolveLinkToMarkdownFile(path: string): Promise<vscode.Uri | undefined> {
|
||||
try {
|
||||
|
||||
@@ -9,6 +9,7 @@ import * as commands from './commands/index';
|
||||
import LinkProvider from './features/documentLinkProvider';
|
||||
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
|
||||
import MarkdownFoldingProvider from './features/foldingProvider';
|
||||
import MarkdownSmartSelect from './features/smartSelect';
|
||||
import { MarkdownContentProvider } from './features/previewContentProvider';
|
||||
import { MarkdownPreviewManager } from './features/previewManager';
|
||||
import MarkdownWorkspaceSymbolProvider from './features/workspaceSymbolProvider';
|
||||
@@ -60,6 +61,7 @@ function registerMarkdownLanguageFeatures(
|
||||
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
|
||||
vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()),
|
||||
vscode.languages.registerFoldingRangeProvider(selector, new MarkdownFoldingProvider(engine)),
|
||||
vscode.languages.registerSelectionRangeProvider(selector, new MarkdownSmartSelect(engine)),
|
||||
vscode.languages.registerWorkspaceSymbolProvider(new MarkdownWorkspaceSymbolProvider(symbolProvider))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,19 +65,6 @@ function getWorkspaceFolder(document: vscode.TextDocument) {
|
||||
|| vscode.workspace.workspaceFolders?.[0]?.uri;
|
||||
}
|
||||
|
||||
function matchAll(
|
||||
pattern: RegExp,
|
||||
text: string
|
||||
): Array<RegExpMatchArray> {
|
||||
const out: RegExpMatchArray[] = [];
|
||||
pattern.lastIndex = 0;
|
||||
let match: RegExpMatchArray | null;
|
||||
while ((match = pattern.exec(text))) {
|
||||
out.push(match);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractDocumentLink(
|
||||
document: vscode.TextDocument,
|
||||
pre: number,
|
||||
@@ -103,9 +90,9 @@ function extractDocumentLink(
|
||||
}
|
||||
|
||||
export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\(\S*?\))+)\s*(".*?")?\)/g;
|
||||
private readonly linkPattern = /(\[((!\[[^\]]*?\]\(\s*)([^\s\(\)]+?)\s*\)\]|(?:\\\]|[^\]])*\])\(\s*)(([^\s\(\)]|\([^\s\(\)]*?\))+)\s*(".*?")?\)/g;
|
||||
private readonly referenceLinkPattern = /(\[((?:\\\]|[^\]])+)\]\[\s*?)([^\s\]]*?)\]/g;
|
||||
private readonly definitionPattern = /^([\t ]*\[((?:\\\]|[^\]])+)\]:\s*)(\S+)/gm;
|
||||
private readonly definitionPattern = /^([\t ]*\[(?!\^)((?:\\\]|[^\]])+)\]:\s*)(\S+)/gm;
|
||||
|
||||
public provideDocumentLinks(
|
||||
document: vscode.TextDocument,
|
||||
@@ -124,7 +111,7 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
document: vscode.TextDocument,
|
||||
): vscode.DocumentLink[] {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
for (const match of matchAll(this.linkPattern, text)) {
|
||||
for (const match of text.matchAll(this.linkPattern)) {
|
||||
const matchImage = match[4] && extractDocumentLink(document, match[3].length + 1, match[4], match.index);
|
||||
if (matchImage) {
|
||||
results.push(matchImage);
|
||||
@@ -144,7 +131,7 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
const results: vscode.DocumentLink[] = [];
|
||||
|
||||
const definitions = this.getDefinitions(text, document);
|
||||
for (const match of matchAll(this.referenceLinkPattern, text)) {
|
||||
for (const match of text.matchAll(this.referenceLinkPattern)) {
|
||||
let linkStart: vscode.Position;
|
||||
let linkEnd: vscode.Position;
|
||||
let reference = match[3];
|
||||
@@ -190,7 +177,7 @@ export default class LinkProvider implements vscode.DocumentLinkProvider {
|
||||
|
||||
private getDefinitions(text: string, document: vscode.TextDocument) {
|
||||
const out = new Map<string, { link: string, linkRange: vscode.Range }>();
|
||||
for (const match of matchAll(this.definitionPattern, text)) {
|
||||
for (const match of text.matchAll(this.definitionPattern)) {
|
||||
const pre = match[1];
|
||||
const reference = match[2];
|
||||
const link = match[3].trim();
|
||||
|
||||
@@ -7,16 +7,9 @@ import { Token } from 'markdown-it';
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
import { flatten } from '../util/arrays';
|
||||
|
||||
const rangeLimit = 5000;
|
||||
|
||||
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.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content));
|
||||
|
||||
export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvider {
|
||||
|
||||
constructor(
|
||||
@@ -33,7 +26,7 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
this.getHeaderFoldingRanges(document),
|
||||
this.getBlockFoldingRanges(document)
|
||||
]);
|
||||
return flatten(foldables).slice(0, rangeLimit);
|
||||
return foldables.flat().slice(0, rangeLimit);
|
||||
}
|
||||
|
||||
private async getRegions(document: vscode.TextDocument): Promise<vscode.FoldingRange[]> {
|
||||
@@ -69,24 +62,6 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
}
|
||||
|
||||
private async getBlockFoldingRanges(document: vscode.TextDocument): Promise<vscode.FoldingRange[]> {
|
||||
|
||||
const isFoldableToken = (token: Token): boolean => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
const tokens = await this.engine.parse(document);
|
||||
const multiLineListItems = tokens.filter(isFoldableToken);
|
||||
return multiLineListItems.map(listItem => {
|
||||
@@ -95,7 +70,36 @@ export default class MarkdownFoldingProvider implements vscode.FoldingRangeProvi
|
||||
if (document.lineAt(end).isEmptyOrWhitespace && end >= start + 1) {
|
||||
end = end - 1;
|
||||
}
|
||||
return new vscode.FoldingRange(start, end, listItem.type === 'html_block' && listItem.content.startsWith('<!--') ? vscode.FoldingRangeKind.Comment : undefined);
|
||||
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.type === 'html_block' && (isStartRegion(token.content) || isEndRegion(token.content));
|
||||
|
||||
const isFoldableToken = (token: Token): boolean => {
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -408,7 +408,7 @@ class MarkdownPreview extends Disposable implements WebviewResourceProvider {
|
||||
}
|
||||
}
|
||||
|
||||
OpenDocumentLinkCommand.execute(this.engine, { path: hrefPath, fragment, fromResource: this.resource.toJSON() });
|
||||
OpenDocumentLinkCommand.execute(this.engine, { parts: { path: hrefPath }, fragment, fromResource: this.resource.toJSON() });
|
||||
}
|
||||
|
||||
//#region WebviewResourceProvider
|
||||
|
||||
@@ -0,0 +1,260 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import { Token } from 'markdown-it';
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { TableOfContentsProvider, TocEntry } from '../tableOfContentsProvider';
|
||||
|
||||
export default class MarkdownSmartSelect implements vscode.SelectionRangeProvider {
|
||||
|
||||
constructor(
|
||||
private readonly engine: MarkdownEngine
|
||||
) { }
|
||||
|
||||
public async provideSelectionRanges(document: vscode.TextDocument, 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: vscode.TextDocument, 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: vscode.TextDocument, position: vscode.Position, blockRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
return createInlineRange(document, position, blockRange);
|
||||
}
|
||||
|
||||
private async getBlockSelectionRange(document: vscode.TextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||
|
||||
const tokens = await this.engine.parse(document);
|
||||
|
||||
const blockTokens = getBlockTokensForPosition(tokens, position);
|
||||
|
||||
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: vscode.TextDocument, position: vscode.Position): Promise<vscode.SelectionRange | undefined> {
|
||||
|
||||
const tocProvider = new TableOfContentsProvider(this.engine, document);
|
||||
const toc = await tocProvider.getToc();
|
||||
|
||||
const headerInfo = getHeadersForPosition(toc, 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));
|
||||
}
|
||||
return currentRange;
|
||||
}
|
||||
}
|
||||
|
||||
function getHeadersForPosition(toc: TocEntry[], position: vscode.Position): { headers: TocEntry[], headerOnThisLine: boolean } {
|
||||
const enclosingHeaders = toc.filter(header => header.location.range.start.line <= position.line && header.location.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.location.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 {
|
||||
// no children and 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): Token[] {
|
||||
const enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && 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: Token, document: vscode.TextDocument, 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: vscode.TextDocument, 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);
|
||||
const linkSelection = createLinkRange(lineText, cursorPosition.character, cursorPosition.line, boldSelection ? boldSelection : italicSelection || parent);
|
||||
const inlineCodeBlockSelection = createOtherInlineRange(lineText, cursorPosition.character, cursorPosition.line, false, linkSelection || parent);
|
||||
return inlineCodeBlockSelection || linkSelection || boldSelection || italicSelection;
|
||||
}
|
||||
|
||||
function createFencedRange(token: Token, cursorLine: number, document: vscode.TextDocument, 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 {
|
||||
// find closest ** that occurs before cursor position
|
||||
let startBold = lineText.substring(0, cursorChar).lastIndexOf('**');
|
||||
|
||||
// find closest ** that occurs after the start **
|
||||
const endBoldIndex = lineText.substring(startBold + 2).indexOf('**');
|
||||
let endBold = startBold + 2 + lineText.substring(startBold + 2).indexOf('**');
|
||||
|
||||
if (startBold >= 0 && endBoldIndex >= 0 && startBold + 1 < endBold && startBold <= cursorChar && endBold >= cursorChar) {
|
||||
const range = new vscode.Range(cursorLine, startBold, cursorLine, endBold + 2);
|
||||
// **content cursor content** so select content then ** on both sides
|
||||
const contentRange = new vscode.Range(cursorLine, startBold + 2, cursorLine, endBold);
|
||||
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent));
|
||||
} else if (startBold >= 0) {
|
||||
// **content**cursor or **content*cursor*
|
||||
// find end ** from end of start ** to end of line (since the cursor is within the end stars)
|
||||
let adjustedEnd = startBold + 2 + lineText.substring(startBold + 2).indexOf('**');
|
||||
startBold = lineText.substring(0, adjustedEnd - 2).lastIndexOf('**');
|
||||
if (adjustedEnd >= 0 && cursorChar === adjustedEnd || cursorChar === adjustedEnd + 1) {
|
||||
if (lineText.charAt(adjustedEnd + 1) === '*') {
|
||||
// *cursor* so need to extend end to include the second *
|
||||
adjustedEnd += 1;
|
||||
}
|
||||
return new vscode.SelectionRange(new vscode.Range(cursorLine, startBold, cursorLine, adjustedEnd + 1), parent);
|
||||
}
|
||||
} else if (endBold > 0) {
|
||||
// cursor**content** or *cursor*content**
|
||||
// find start ** from start of string to cursor + 2 (since the cursor is within the start stars)
|
||||
const adjustedStart = lineText.substring(0, cursorChar + 2).lastIndexOf('**');
|
||||
endBold = adjustedStart + 2 + lineText.substring(adjustedStart + 2).indexOf('**');
|
||||
if (adjustedStart >= 0 && adjustedStart === cursorChar || adjustedStart === cursorChar - 1) {
|
||||
return new vscode.SelectionRange(new vscode.Range(cursorLine, adjustedStart, cursorLine, endBold + 2), parent);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createOtherInlineRange(lineText: string, cursorChar: number, cursorLine: number, isItalic: boolean, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||
const type = isItalic ? '*' : '`';
|
||||
const start = lineText.substring(0, cursorChar + 1).lastIndexOf(type);
|
||||
let end = lineText.substring(cursorChar).indexOf(type);
|
||||
|
||||
if (start >= 0 && end >= 0) {
|
||||
end += cursorChar;
|
||||
// ensure there's no * or ` before end
|
||||
const intermediate = lineText.substring(start + 1, end - 1).indexOf(type);
|
||||
if (intermediate < 0) {
|
||||
const range = new vscode.Range(cursorLine, start, cursorLine, end + 1);
|
||||
if (cursorChar > start && cursorChar <= end) {
|
||||
// within the content so select content then include the stars or backticks
|
||||
const contentRange = new vscode.Range(cursorLine, start + 1, cursorLine, end);
|
||||
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(range, parent));
|
||||
} else if (cursorChar === start) {
|
||||
return new vscode.SelectionRange(range, parent);
|
||||
}
|
||||
}
|
||||
}
|
||||
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 > 0) {
|
||||
// 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;
|
||||
|
||||
// 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 === lineText.indexOf(nearestType) || cursorChar === lineText.indexOf(nearestType) + nearestType.length;
|
||||
|
||||
const contentAndNearestType = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(nearestType), cursorLine, lineText.indexOf(nearestType) + nearestType.length), linkRange);
|
||||
const content = new vscode.SelectionRange(new vscode.Range(cursorLine, lineText.indexOf(nearestType) + 1, cursorLine, lineText.indexOf(nearestType) + 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: vscode.TextDocument, header?: TocEntry, toc?: TocEntry[]): vscode.Position | undefined {
|
||||
let childRange: vscode.Position | undefined;
|
||||
if (header && toc) {
|
||||
let children = toc.filter(t => header.location.range.contains(t.location.range) && t.location.range.start.line > header.location.range.start.line).sort((t1, t2) => t1.line - t2.line);
|
||||
if (children.length > 0) {
|
||||
childRange = children[0].location.range.start;
|
||||
const lineText = document.lineAt(childRange.line - 1).text;
|
||||
return childRange ? childRange.translate(-1, lineText.length) : undefined;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -9,7 +9,6 @@ import { isMarkdownFile } from '../util/file';
|
||||
import { Lazy, lazy } from '../util/lazy';
|
||||
import MDDocumentSymbolProvider from './documentSymbolProvider';
|
||||
import { SkinnyTextDocument, SkinnyTextLine } from '../tableOfContentsProvider';
|
||||
import { flatten } from '../util/arrays';
|
||||
|
||||
export interface WorkspaceMarkdownDocumentProvider {
|
||||
getAllMarkdownDocuments(): Thenable<Iterable<SkinnyTextDocument>>;
|
||||
@@ -136,7 +135,7 @@ export default class MarkdownWorkspaceSymbolProvider extends Disposable implemen
|
||||
}
|
||||
|
||||
const allSymbolsSets = await Promise.all(Array.from(this._symbolCache.values()).map(x => x.value));
|
||||
const allSymbols = flatten(allSymbolsSets);
|
||||
const allSymbols = allSymbolsSets.flat();
|
||||
return allSymbols.filter(symbolInformation => symbolInformation.name.toLowerCase().indexOf(query.toLowerCase()) !== -1);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,19 +57,19 @@ export class TableOfContentsProvider {
|
||||
const toc: TocEntry[] = [];
|
||||
const tokens = await this.engine.parse(document);
|
||||
|
||||
const slugCount = new Map<string, number>();
|
||||
const existingSlugEntries = new Map<string, { count: number }>();
|
||||
|
||||
for (const heading of tokens.filter(token => token.type === 'heading_open')) {
|
||||
const lineNumber = heading.map[0];
|
||||
const line = document.lineAt(lineNumber);
|
||||
|
||||
let slug = githubSlugifier.fromHeading(line.text);
|
||||
if (slugCount.has(slug.value)) {
|
||||
const count = slugCount.get(slug.value)!;
|
||||
slugCount.set(slug.value, count + 1);
|
||||
slug = githubSlugifier.fromHeading(slug.value + '-' + (count + 1));
|
||||
const existingSlugEntry = existingSlugEntries.get(slug.value);
|
||||
if (existingSlugEntry) {
|
||||
++existingSlugEntry.count;
|
||||
slug = githubSlugifier.fromHeading(slug.value + '-' + existingSlugEntry.count);
|
||||
} else {
|
||||
slugCount.set(slug.value, 0);
|
||||
existingSlugEntries.set(slug.value, { count: 0 });
|
||||
}
|
||||
|
||||
toc.push({
|
||||
@@ -91,7 +91,7 @@ export class TableOfContentsProvider {
|
||||
break;
|
||||
}
|
||||
}
|
||||
const endLine = end !== undefined ? end : document.lineCount - 1;
|
||||
const endLine = end ?? document.lineCount - 1;
|
||||
return {
|
||||
...entry,
|
||||
location: new vscode.Location(document.uri,
|
||||
|
||||
@@ -138,6 +138,12 @@ suite('markdown.DocumentLinkProvider', () => {
|
||||
assertRangeEqual(link4.range, new vscode.Range(0, 50, 0, 59));
|
||||
}
|
||||
});
|
||||
|
||||
// #107471
|
||||
test('Should not consider link references starting with ^ character valid', () => {
|
||||
const links = getLinksForFile('[^reference]: https://example.com');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import * as os from 'os';
|
||||
export class InMemoryDocument implements vscode.TextDocument {
|
||||
private readonly _lines: string[];
|
||||
|
||||
@@ -13,7 +13,7 @@ export class InMemoryDocument implements vscode.TextDocument {
|
||||
private readonly _contents: string,
|
||||
public readonly version = 1,
|
||||
) {
|
||||
this._lines = this._contents.split(/\n/g);
|
||||
this._lines = this._contents.split(/\r\n|\n/g);
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export class InMemoryDocument implements vscode.TextDocument {
|
||||
languageId: string = '';
|
||||
isDirty: boolean = false;
|
||||
isClosed: boolean = false;
|
||||
eol: vscode.EndOfLine = vscode.EndOfLine.LF;
|
||||
eol: vscode.EndOfLine = os.platform() === 'win32' ? vscode.EndOfLine.CRLF : vscode.EndOfLine.LF;
|
||||
notebook: undefined;
|
||||
|
||||
get fileName(): string {
|
||||
@@ -47,9 +47,9 @@ export class InMemoryDocument implements vscode.TextDocument {
|
||||
}
|
||||
positionAt(offset: number): vscode.Position {
|
||||
const before = this._contents.slice(0, offset);
|
||||
const newLines = before.match(/\n/g);
|
||||
const newLines = before.match(/\r\n|\n/g);
|
||||
const line = newLines ? newLines.length : 0;
|
||||
const preCharacters = before.match(/(\n|^).*$/g);
|
||||
const preCharacters = before.match(/(\r\n|\n|^).*$/g);
|
||||
return new vscode.Position(line, preCharacters ? preCharacters[0].length : 0);
|
||||
}
|
||||
getText(_range?: vscode.Range | undefined): string {
|
||||
|
||||
@@ -0,0 +1,630 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
import MarkdownSmartSelect from '../features/smartSelect';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { joinLines } from './util';
|
||||
const CURSOR = '$$CURSOR$$';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.SmartSelect', () => {
|
||||
test('Smart select single word', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(`Hel${CURSOR}lo`);
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0]);
|
||||
});
|
||||
test('Smart select multi-line paragraph', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`Many of the core components and extensions to ${CURSOR}VS Code live in their own repositories on GitHub. `,
|
||||
`For example, the[node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter]`,
|
||||
`(https://github.com/microsoft/vscode-mono-debug) have their own repositories. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki).`
|
||||
));
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
test('Smart select paragraph', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(`Many of the core components and extensions to ${CURSOR}VS Code live in their own repositories on GitHub. For example, the [node debug adapter](https://github.com/microsoft/vscode-node-debug) and the [mono debug adapter](https://github.com/microsoft/vscode-mono-debug) have their own repositories. For a complete list, please visit the [Related Projects](https://github.com/microsoft/vscode/wiki/Related-Projects) page on our [wiki](https://github.com/microsoft/vscode/wiki).`);
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0]);
|
||||
});
|
||||
test('Smart select html block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`<p align="center">`,
|
||||
`${CURSOR}<img alt="VS Code in action" src="https://user-images.githubusercontent.com/1487073/58344409-70473b80-7e0a-11e9-8570-b2efc6f8fa44.png">`,
|
||||
`</p>`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
test('Smart select header on header line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# Header${CURSOR}`,
|
||||
`Hello`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 1]);
|
||||
|
||||
});
|
||||
test('Smart select single word w grandparent header on text line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`## ParentHeader`,
|
||||
`# Header`,
|
||||
`${CURSOR}Hello`
|
||||
));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [1, 2]);
|
||||
});
|
||||
test('Smart select html block w parent header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# Header`,
|
||||
`${CURSOR}<p align="center">`,
|
||||
`<img alt="VS Code in action" src="https://user-images.githubusercontent.com/1487073/58344409-70473b80-7e0a-11e9-8570-b2efc6f8fa44.png">`,
|
||||
`</p>`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select fenced code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`~~~`,
|
||||
`a${CURSOR}`,
|
||||
`~~~`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
test('Smart select list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- item 1`,
|
||||
`- ${CURSOR}item 2`,
|
||||
`- item 3`,
|
||||
`- item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [0, 3]);
|
||||
});
|
||||
test('Smart select list with fenced code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- item 1`,
|
||||
`- ~~~`,
|
||||
` ${CURSOR}a`,
|
||||
` ~~~`,
|
||||
`- item 3`,
|
||||
`- item 4`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 5]);
|
||||
});
|
||||
test('Smart select multi cursor', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- ${CURSOR}item 1`,
|
||||
`- ~~~`,
|
||||
` a`,
|
||||
` ~~~`,
|
||||
`- ${CURSOR}item 3`,
|
||||
`- item 4`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 0], [0, 5]);
|
||||
assertNestedLineNumbersEqual(ranges![1], [4, 4], [0, 5]);
|
||||
});
|
||||
test('Smart select nested block quotes', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`> item 1`,
|
||||
`> item 2`,
|
||||
`>> ${CURSOR}item 3`,
|
||||
`>> item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select multi nested block quotes', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`> item 1`,
|
||||
`>> item 2`,
|
||||
`>>> ${CURSOR}item 3`,
|
||||
`>>>> item 4`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select subheader content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`## sub header 1`,
|
||||
`${CURSOR}content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select subheader line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`## sub header 1${CURSOR}`,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 3], [1, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select blank line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`${CURSOR} `,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select line between paragraphs', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`paragraph 1`,
|
||||
`${CURSOR}`,
|
||||
`paragraph 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 2]);
|
||||
});
|
||||
test('Smart select empty document', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(``, [new vscode.Position(0, 0)]);
|
||||
assert.strictEqual(ranges!.length, 0);
|
||||
});
|
||||
test('Smart select fenced code block then list then subheader content then subheader then header content then header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
`content 1`,
|
||||
`## sub header 1`,
|
||||
`- item 1`,
|
||||
`- ~~~`,
|
||||
` ${CURSOR}a`,
|
||||
` ~~~`,
|
||||
`- item 3`,
|
||||
`- item 4`,
|
||||
``,
|
||||
`more content`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [4, 6], [3, 9], [3, 10], [2, 10], [1, 10], [0, 10]);
|
||||
});
|
||||
test('Smart select list with one element without selecting child subheader', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list ${CURSOR}`,
|
||||
``,
|
||||
`## sub header`,
|
||||
``,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [1, 3], [1, 6], [0, 6]);
|
||||
});
|
||||
test('Smart select content under header then subheaders and their content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main ${CURSOR}header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [0, 3], [0, 6]);
|
||||
});
|
||||
test('Smart select last blockquote element under header then subheaders and their content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> ${CURSOR}block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
`content 2`,
|
||||
`# main header 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [5, 5], [4, 5], [2, 5], [1, 7], [1, 10], [0, 10]);
|
||||
});
|
||||
test('Smart select content of subheader then subheader then content of main header then main header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
``,
|
||||
`${CURSOR}`,
|
||||
``,
|
||||
`### main header 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [11, 11], [9, 12], [9, 17], [8, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
test('Smart select last line content of subheader then subheader then content of main header then main header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
`### main header 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- ${CURSOR}content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [17, 17], [14, 17], [13, 17], [9, 17], [8, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
test('Smart select last line content after content of subheader then subheader then content of main header then main header', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
``,
|
||||
`### main header 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2${CURSOR}`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [17, 17], [14, 17], [13, 17], [9, 17], [8, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
test('Smart select fenced code block then list then rest of content', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`- paragraph`,
|
||||
`- ~~~`,
|
||||
` my`,
|
||||
` ${CURSOR}code`,
|
||||
` goes here`,
|
||||
` ~~~`,
|
||||
`- content`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [9, 11], [8, 12], [8, 12], [7, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
test('Smart select fenced code block then list then rest of content on fenced line', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`> block`,
|
||||
`> block`,
|
||||
`>> block`,
|
||||
`>> block`,
|
||||
``,
|
||||
`- paragraph`,
|
||||
`- ~~~${CURSOR}`,
|
||||
` my`,
|
||||
` code`,
|
||||
` goes here`,
|
||||
` ~~~`,
|
||||
`- content`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`,
|
||||
`- content 2`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [8, 12], [7, 17], [1, 17], [0, 17]);
|
||||
});
|
||||
test('Smart select without multiple ranges', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
``,
|
||||
`- ${CURSOR}paragraph`,
|
||||
`- content`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [1, 4], [0, 4]);
|
||||
});
|
||||
test('Smart select on second level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}1`,
|
||||
`* level 0`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [5, 5], [1, 5], [0, 5], [0, 6]);
|
||||
});
|
||||
test('Smart select on third level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
`* level 0`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [2, 4], [1, 6], [0, 6], [0, 7]);
|
||||
});
|
||||
test('Smart select level 2 then level 1', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
`* level 1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 2], [0, 2], [0, 3]);
|
||||
});
|
||||
test('Smart select last list item', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`- level 1`,
|
||||
`- level 2`,
|
||||
`- level 2`,
|
||||
`- level ${CURSOR}1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [0, 3]);
|
||||
});
|
||||
test('Smart select without multiple ranges', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
``,
|
||||
`- ${CURSOR}paragraph`,
|
||||
`- content`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [1, 4], [0, 4]);
|
||||
});
|
||||
test('Smart select on second level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}1`,
|
||||
`* level 0`));
|
||||
|
||||
assertNestedLineNumbersEqual(ranges![0], [5, 5], [1, 5], [0, 5], [0, 6]);
|
||||
});
|
||||
test('Smart select on third level of a list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 0`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
` * level 1`,
|
||||
` * level 1`,
|
||||
`* level 0`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [3, 3], [3, 4], [2, 4], [1, 6], [0, 6], [0, 7]);
|
||||
});
|
||||
test('Smart select level 2 then level 1', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`* level 1`,
|
||||
` * level ${CURSOR}2`,
|
||||
` * level 2`,
|
||||
`* level 1`));
|
||||
assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 2], [0, 2], [0, 3]);
|
||||
});
|
||||
test('Smart select bold', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`stuff here **new${CURSOR}item** and here`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 13, 0, 30], [0, 11, 0, 32], [0, 0, 0, 41]);
|
||||
});
|
||||
test('Smart select link', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`stuff here [text](https${CURSOR}://google.com) and here`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 18, 0, 46], [0, 17, 0, 47], [0, 11, 0, 47], [0, 0, 0, 56]);
|
||||
});
|
||||
test('Smart select brackets', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`stuff here [te${CURSOR}xt](https://google.com) and here`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 12, 0, 26], [0, 11, 0, 27], [0, 11, 0, 47], [0, 0, 0, 56]);
|
||||
});
|
||||
test('Smart select brackets under header in list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
`- list`,
|
||||
`- stuff here [te${CURSOR}xt](https://google.com) and here`,
|
||||
`- list`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [6, 14, 6, 28], [6, 13, 6, 29], [6, 13, 6, 49], [6, 0, 6, 58], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]);
|
||||
});
|
||||
test('Smart select link under header in list', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
`- list`,
|
||||
`- stuff here [text](${CURSOR}https://google.com) and here`,
|
||||
`- list`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [6, 20, 6, 48], [6, 19, 6, 49], [6, 13, 6, 49], [6, 0, 6, 58], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]);
|
||||
});
|
||||
test('Smart select bold within list where multiple bold elements exists', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`# main header 1`,
|
||||
``,
|
||||
`- list`,
|
||||
`paragraph`,
|
||||
`## sub header`,
|
||||
`- list`,
|
||||
`- stuff here [text]**${CURSOR}items in here** and **here**`,
|
||||
`- list`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [6, 21, 6, 44], [6, 19, 6, 46], [6, 0, 6, 59], [5, 0, 7, 6], [4, 0, 7, 6], [1, 0, 7, 6], [0, 0, 7, 6]);
|
||||
});
|
||||
test('Smart select link in paragraph with multiple links', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`This[extension](https://marketplace.visualstudio.com/items?itemName=meganrogge.template-string-converter) addresses this [requ${CURSOR}est](https://github.com/microsoft/vscode/issues/56704) to convert Javascript/Typescript quotes to backticks when has been entered within a string.`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 123, 0, 140], [0, 122, 0, 141], [0, 122, 0, 191], [0, 0, 0, 283]);
|
||||
});
|
||||
test('Smart select bold link', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`**[extens${CURSOR}ion](https://google.com)**`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 3, 0, 22], [0, 2, 0, 23], [0, 2, 0, 43], [0, 2, 0, 43], [0, 0, 0, 45], [0, 0, 0, 45]);
|
||||
});
|
||||
test('Smart select inline code block', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`[\`code ${CURSOR} link\`]`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 2, 0, 22], [0, 1, 0, 23], [0, 0, 0, 24]);
|
||||
});
|
||||
test('Smart select link with inline code block text', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`[\`code ${CURSOR} link\`](http://example.com)`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 2, 0, 22], [0, 1, 0, 23], [0, 1, 0, 23], [0, 0, 0, 24], [0, 0, 0, 44], [0, 0, 0, 44]);
|
||||
});
|
||||
test('Smart select italic', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`*some nice ${CURSOR}text*`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 1, 0, 25], [0, 0, 0, 26], [0, 0, 0, 26]);
|
||||
});
|
||||
test('Smart select italic link', async () => {
|
||||
const ranges = await getSelectionRangesForDocument(
|
||||
joinLines(
|
||||
`*[extens${CURSOR}ion](https://google.com)*`
|
||||
));
|
||||
assertNestedRangesEqual(ranges![0], [0, 2, 0, 21], [0, 1, 0, 22], [0, 1, 0, 42], [0, 1, 0, 42], [0, 0, 0, 43], [0, 0, 0, 43]);
|
||||
});
|
||||
});
|
||||
|
||||
function assertNestedLineNumbersEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number][]) {
|
||||
const lineage = getLineage(range);
|
||||
assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length}`);
|
||||
for (let i = 0; i < lineage.length; i++) {
|
||||
assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNestedRangesEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number, number, number][]) {
|
||||
const lineage = getLineage(range);
|
||||
assert.strictEqual(lineage.length, expectedRanges.length, `expected depth: ${expectedRanges.length}, but was ${lineage.length} ${getValues(lineage)}`);
|
||||
for (let i = 0; i < lineage.length; i++) {
|
||||
assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][2], `parent at a depth of ${i}`);
|
||||
assert(lineage[i].range.start.character === expectedRanges[i][1], `parent at a depth of ${i} on start char`);
|
||||
assert(lineage[i].range.end.character === expectedRanges[i][3], `parent at a depth of ${i} on end char`);
|
||||
}
|
||||
}
|
||||
|
||||
function getLineage(range: vscode.SelectionRange): vscode.SelectionRange[] {
|
||||
const result: vscode.SelectionRange[] = [];
|
||||
let currentRange: vscode.SelectionRange | undefined = range;
|
||||
while (currentRange) {
|
||||
result.push(currentRange);
|
||||
currentRange = currentRange.parent;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getValues(ranges: vscode.SelectionRange[]): string[] {
|
||||
return ranges.map(range => {
|
||||
return range.range.start.line + ' ' + range.range.start.character + ' ' + range.range.end.line + ' ' + range.range.end.character;
|
||||
});
|
||||
}
|
||||
|
||||
function assertLineNumbersEqual(selectionRange: vscode.SelectionRange, startLine: number, endLine: number, message: string) {
|
||||
assert.strictEqual(selectionRange.range.start.line, startLine, `failed on start line ${message}`);
|
||||
assert.strictEqual(selectionRange.range.end.line, endLine, `failed on end line ${message}`);
|
||||
}
|
||||
|
||||
async function getSelectionRangesForDocument(contents: string, pos?: vscode.Position[]) {
|
||||
const doc = new InMemoryDocument(testFileName, contents);
|
||||
const provider = new MarkdownSmartSelect(createNewMarkdownEngine());
|
||||
const positions = pos ? pos : getCursorPositions(contents, doc);
|
||||
return await provider.provideSelectionRanges(doc, positions, new vscode.CancellationTokenSource().token);
|
||||
}
|
||||
|
||||
let getCursorPositions = (contents: string, doc: InMemoryDocument): vscode.Position[] => {
|
||||
let positions: vscode.Position[] = [];
|
||||
let index = 0;
|
||||
let wordLength = 0;
|
||||
while (index !== -1) {
|
||||
index = contents.indexOf(CURSOR, index + wordLength);
|
||||
if (index !== -1) {
|
||||
positions.push(doc.positionAt(index));
|
||||
}
|
||||
wordLength = CURSOR.length;
|
||||
}
|
||||
return positions;
|
||||
};
|
||||
@@ -15,8 +15,4 @@ export function equals<T>(one: ReadonlyArray<T>, other: ReadonlyArray<T>, itemEq
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function flatten<T>(arr: ReadonlyArray<T>[]): T[] {
|
||||
return ([] as T[]).concat.apply([], arr);
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
[b](b)
|
||||
[b](b.md)
|
||||
[b](./b.md)
|
||||
[b](/b.md)
|
||||
|
||||
[b.md](b.md)
|
||||
|
||||
[./b.md](./b.md)
|
||||
|
||||
[/b.md](/b.md)
|
||||
|
||||
[b#header1](b#header1)
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# b
|
||||
|
||||
[](./a)
|
||||
[./a](./a)
|
||||
|
||||
# header1
|
||||
@@ -6,6 +6,8 @@
|
||||
"lib": [
|
||||
"es6",
|
||||
"es2015.promise",
|
||||
"es2019.array",
|
||||
"es2020.string",
|
||||
"dom"
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user