parent
885e37289a
commit
5d4df8273e
3 changed files with 651 additions and 0 deletions
|
@ -9,6 +9,7 @@ import * as commands from './commands/index';
|
||||||
import LinkProvider from './features/documentLinkProvider';
|
import LinkProvider from './features/documentLinkProvider';
|
||||||
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
|
import MDDocumentSymbolProvider from './features/documentSymbolProvider';
|
||||||
import MarkdownFoldingProvider from './features/foldingProvider';
|
import MarkdownFoldingProvider from './features/foldingProvider';
|
||||||
|
import MarkdownSmartSelect from './features/smartSelect';
|
||||||
import { MarkdownContentProvider } from './features/previewContentProvider';
|
import { MarkdownContentProvider } from './features/previewContentProvider';
|
||||||
import { MarkdownPreviewManager } from './features/previewManager';
|
import { MarkdownPreviewManager } from './features/previewManager';
|
||||||
import MarkdownWorkspaceSymbolProvider from './features/workspaceSymbolProvider';
|
import MarkdownWorkspaceSymbolProvider from './features/workspaceSymbolProvider';
|
||||||
|
@ -60,6 +61,7 @@ function registerMarkdownLanguageFeatures(
|
||||||
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
|
vscode.languages.registerDocumentSymbolProvider(selector, symbolProvider),
|
||||||
vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()),
|
vscode.languages.registerDocumentLinkProvider(selector, new LinkProvider()),
|
||||||
vscode.languages.registerFoldingRangeProvider(selector, new MarkdownFoldingProvider(engine)),
|
vscode.languages.registerFoldingRangeProvider(selector, new MarkdownFoldingProvider(engine)),
|
||||||
|
vscode.languages.registerSelectionRangeProvider(selector, new MarkdownSmartSelect(engine)),
|
||||||
vscode.languages.registerWorkspaceSymbolProvider(new MarkdownWorkspaceSymbolProvider(symbolProvider))
|
vscode.languages.registerWorkspaceSymbolProvider(new MarkdownWorkspaceSymbolProvider(symbolProvider))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. 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> {
|
||||||
|
let 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);
|
||||||
|
return blockRange ? blockRange : headerRange ? headerRange : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getBlockSelectionRange(document: vscode.TextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise<vscode.SelectionRange | undefined> {
|
||||||
|
|
||||||
|
const tokens = await this.engine.parse(document);
|
||||||
|
|
||||||
|
let blockTokens = getTokensForPosition(tokens, position);
|
||||||
|
|
||||||
|
if (blockTokens.length === 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parentRange = headerRange ? headerRange : createBlockRange(document, position.line, blockTokens.shift());
|
||||||
|
let currentRange: vscode.SelectionRange | undefined;
|
||||||
|
|
||||||
|
for (const token of blockTokens) {
|
||||||
|
currentRange = createBlockRange(document, position.line, token, parentRange);
|
||||||
|
if (currentRange) {
|
||||||
|
parentRange = currentRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (currentRange) {
|
||||||
|
return currentRange;
|
||||||
|
} else {
|
||||||
|
return parentRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
let headerInfo = getHeadersForPosition(toc, position);
|
||||||
|
|
||||||
|
let headers = headerInfo.headers;
|
||||||
|
|
||||||
|
let parentRange: vscode.SelectionRange | undefined;
|
||||||
|
let currentRange: vscode.SelectionRange | undefined;
|
||||||
|
|
||||||
|
for (let i = 0; i < headers.length; i++) {
|
||||||
|
currentRange = createHeaderRange(i === headers.length - 1, headerInfo.headerOnThisLine, headers[i], parentRange, getFirstChildHeader(document, headers[i], toc));
|
||||||
|
if (currentRange && currentRange.parent) {
|
||||||
|
parentRange = currentRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
let lineText = document.lineAt(childRange.line - 1).text;
|
||||||
|
return childRange ? childRange.translate(-1, lineText.length) : undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTokensForPosition(tokens: Token[], position: vscode.Position): Token[] {
|
||||||
|
let enclosingTokens = tokens.filter(token => token.map && (token.map[0] <= position.line && token.map[1] > position.line) && isBlockElement(token));
|
||||||
|
|
||||||
|
if (enclosingTokens.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortedTokens = enclosingTokens.sort((token1, token2) => (token2.map[1] - token2.map[0]) - (token1.map[1] - token1.map[0]));
|
||||||
|
return sortedTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getHeadersForPosition(toc: TocEntry[], position: vscode.Position): { headers: TocEntry[], headerOnThisLine: boolean } {
|
||||||
|
let enclosingHeaders = toc.filter(header => header.location.range.start.line <= position.line && header.location.range.end.line >= position.line);
|
||||||
|
let sortedHeaders = enclosingHeaders.sort((header1, header2) => (header1.line - position.line) - (header2.line - position.line));
|
||||||
|
let onThisLine = toc.find(header => header.line === position.line) !== undefined;
|
||||||
|
return {
|
||||||
|
headers: sortedHeaders,
|
||||||
|
headerOnThisLine: onThisLine
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBlockElement(token: Token): boolean {
|
||||||
|
return !['list_item_close', 'paragraph_close', 'bullet_list_close', 'inline', 'heading_close', 'heading_open'].includes(token.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createHeaderRange(isClosestHeaderToPosition: boolean, onHeaderLine: boolean, header?: TocEntry, parent?: vscode.SelectionRange, childStart?: vscode.Position): vscode.SelectionRange | undefined {
|
||||||
|
if (header) {
|
||||||
|
let contentRange = new vscode.Range(header.location.range.start.translate(1), header.location.range.end);
|
||||||
|
let headerPlusContentRange = header.location.range;
|
||||||
|
let partialContentRange = childStart && isClosestHeaderToPosition ? contentRange.with(undefined, childStart) : undefined;
|
||||||
|
if (onHeaderLine && isClosestHeaderToPosition && childStart) {
|
||||||
|
return new vscode.SelectionRange(header.location.range.with(undefined, childStart), new vscode.SelectionRange(header.location.range, parent));
|
||||||
|
} else if (onHeaderLine && isClosestHeaderToPosition) {
|
||||||
|
return new vscode.SelectionRange(header.location.range, parent);
|
||||||
|
} else if (parent && parent.range.contains(headerPlusContentRange)) {
|
||||||
|
if (partialContentRange) {
|
||||||
|
return new vscode.SelectionRange(partialContentRange, new vscode.SelectionRange(contentRange, (new vscode.SelectionRange(headerPlusContentRange, parent))));
|
||||||
|
} else {
|
||||||
|
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(headerPlusContentRange, parent));
|
||||||
|
}
|
||||||
|
} else if (partialContentRange) {
|
||||||
|
return new vscode.SelectionRange(partialContentRange, new vscode.SelectionRange(contentRange, (new vscode.SelectionRange(headerPlusContentRange))));
|
||||||
|
} else {
|
||||||
|
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(headerPlusContentRange));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBlockRange(document: vscode.TextDocument, cursorLine: number, block?: Token, parent?: vscode.SelectionRange): vscode.SelectionRange | undefined {
|
||||||
|
if (block) {
|
||||||
|
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] && isList(block.type) ? block.map[1] - 1 : block.map[1];
|
||||||
|
let startPos = new vscode.Position(startLine, 0);
|
||||||
|
let endPos = new vscode.Position(endLine, getEndCharacter(document, startLine, endLine));
|
||||||
|
let range = new vscode.Range(startPos, endPos);
|
||||||
|
if (parent && parent.range.contains(range) && !parent.range.isEqual(range)) {
|
||||||
|
return new vscode.SelectionRange(range, parent);
|
||||||
|
} else if (parent) {
|
||||||
|
if (rangeLinesEqual(range, parent.range)) {
|
||||||
|
return range.end.character > parent.range.end.character ? new vscode.SelectionRange(range) : parent;
|
||||||
|
} else if (parent.range.end.line + 1 === range.end.line) {
|
||||||
|
let adjustedRange = new vscode.Range(range.start, range.end.translate(-1, parent.range.end.character));
|
||||||
|
if (adjustedRange.isEqual(parent.range)) {
|
||||||
|
return parent;
|
||||||
|
} else {
|
||||||
|
return new vscode.SelectionRange(adjustedRange, parent);
|
||||||
|
}
|
||||||
|
} else if (parent.range.end.line === range.end.line) {
|
||||||
|
let adjustedRange = new vscode.Range(parent.range.start, range.end.translate(undefined, parent.range.end.character));
|
||||||
|
if (adjustedRange.isEqual(parent.range)) {
|
||||||
|
return parent;
|
||||||
|
} else {
|
||||||
|
return new vscode.SelectionRange(adjustedRange, parent.parent);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new vscode.SelectionRange(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
let onFenceLine = cursorLine === startLine || cursorLine === endLine;
|
||||||
|
let fenceRange = new vscode.Range(new vscode.Position(startLine, 0), new vscode.Position(endLine, document.lineAt(endLine).text.length));
|
||||||
|
let contentRange = endLine - startLine > 2 && !onFenceLine ? new vscode.Range(new vscode.Position(startLine + 1, 0), new vscode.Position(endLine - 1, getEndCharacter(document, startLine + 1, endLine))) : undefined;
|
||||||
|
if (parent && contentRange) {
|
||||||
|
if (parent.range.contains(fenceRange) && !parent.range.isEqual(fenceRange)) {
|
||||||
|
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(fenceRange, parent));
|
||||||
|
} else if (parent.range.isEqual(fenceRange)) {
|
||||||
|
return new vscode.SelectionRange(contentRange, parent);
|
||||||
|
} else if (rangeLinesEqual(fenceRange, parent.range)) {
|
||||||
|
let revisedRange = fenceRange.end.character > parent.range.end.character ? fenceRange : parent.range;
|
||||||
|
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(revisedRange, getRealParent(parent, revisedRange)));
|
||||||
|
} else if (parent.range.end.line === fenceRange.end.line) {
|
||||||
|
parent.range.end.translate(undefined, fenceRange.end.character);
|
||||||
|
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(fenceRange, parent));
|
||||||
|
}
|
||||||
|
} else if (contentRange) {
|
||||||
|
return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(fenceRange));
|
||||||
|
} else if (parent) {
|
||||||
|
if (parent.range.contains(fenceRange) && !parent.range.isEqual(fenceRange)) {
|
||||||
|
return new vscode.SelectionRange(fenceRange, parent);
|
||||||
|
} else if (parent.range.isEqual(fenceRange)) {
|
||||||
|
return parent;
|
||||||
|
} else if (rangeLinesEqual(fenceRange, parent.range)) {
|
||||||
|
let revisedRange = fenceRange.end.character > parent.range.end.character ? fenceRange : parent.range;
|
||||||
|
return new vscode.SelectionRange(revisedRange, parent.parent);
|
||||||
|
} else if (parent.range.end.line === fenceRange.end.line) {
|
||||||
|
parent.range.end.translate(undefined, fenceRange.end.character);
|
||||||
|
return new vscode.SelectionRange(fenceRange, parent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new vscode.SelectionRange(fenceRange, parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isList(type: string): boolean {
|
||||||
|
return type ? ['ordered_list_open', 'list_item_open', 'bullet_list_open'].includes(type) : false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEndCharacter(document: vscode.TextDocument, startLine: number, endLine: number): number {
|
||||||
|
let startLength = document.lineAt(startLine).text ? document.lineAt(startLine).text.length : 0;
|
||||||
|
let endLength = document.lineAt(startLine).text ? document.lineAt(startLine).text.length : 0;
|
||||||
|
let endChar = Math.max(startLength, endLength);
|
||||||
|
return startLine !== endLine ? 0 : endChar;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRealParent(parent: vscode.SelectionRange, range: vscode.Range) {
|
||||||
|
let currentParent: vscode.SelectionRange | undefined = parent;
|
||||||
|
while (currentParent && !currentParent.range.contains(range)) {
|
||||||
|
currentParent = currentParent.parent;
|
||||||
|
}
|
||||||
|
return currentParent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rangeLinesEqual(range: vscode.Range, parent: vscode.Range) {
|
||||||
|
return range.start.line === parent.start.line && range.end.line === parent.end.line;
|
||||||
|
}
|
|
@ -0,0 +1,411 @@
|
||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the MIT License. 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.only('markdown.SmartSelect', () => {
|
||||||
|
test('Smart select single word', async () => {
|
||||||
|
const ranges = await getSelectionRangesForDocument(`Hel${CURSOR}lo`);
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 1]);
|
||||||
|
});
|
||||||
|
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).`
|
||||||
|
));
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 3]);
|
||||||
|
});
|
||||||
|
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).`);
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 1]);
|
||||||
|
});
|
||||||
|
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>`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 3]);
|
||||||
|
});
|
||||||
|
test('Smart select header on header line', async () => {
|
||||||
|
const ranges = await getSelectionRangesForDocument(
|
||||||
|
joinLines(
|
||||||
|
`# Header${CURSOR}`,
|
||||||
|
`Hello`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`
|
||||||
|
));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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>`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [1, 3], [1, 3], [0, 3]);
|
||||||
|
});
|
||||||
|
test('Smart select fenced code block', async () => {
|
||||||
|
const ranges = await getSelectionRangesForDocument(
|
||||||
|
joinLines(
|
||||||
|
`~~~`,
|
||||||
|
`a${CURSOR}`,
|
||||||
|
`~~~`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 2]);
|
||||||
|
});
|
||||||
|
test('Smart select list', async () => {
|
||||||
|
const ranges = await getSelectionRangesForDocument(
|
||||||
|
joinLines(
|
||||||
|
`- item 1`,
|
||||||
|
`- ${CURSOR}item 2`,
|
||||||
|
`- item 3`,
|
||||||
|
`- item 4`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 0], [0, 5]);
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
assertNestedRangesEqual(ranges![0], [2, 4], [0, 4]);
|
||||||
|
});
|
||||||
|
test('Smart select multi nested block quotes', async () => {
|
||||||
|
const ranges = await getSelectionRangesForDocument(
|
||||||
|
joinLines(
|
||||||
|
`> item 1`,
|
||||||
|
`>> item 2`,
|
||||||
|
`>>> ${CURSOR}item 3`,
|
||||||
|
`>>>> item 4`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [2, 3], [2, 4], [1, 4], [0, 4]);
|
||||||
|
});
|
||||||
|
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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [1, 3], [0, 3]);
|
||||||
|
});
|
||||||
|
test('Smart select line between paragraphs', async () => {
|
||||||
|
const ranges = await getSelectionRangesForDocument(
|
||||||
|
joinLines(
|
||||||
|
`paragraph 1`,
|
||||||
|
`${CURSOR}`,
|
||||||
|
`paragraph 2`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [0, 3]);
|
||||||
|
});
|
||||||
|
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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [4, 6], [2, 6], [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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [11, 12], [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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [16, 17], [14, 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}`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [16, 17], [14, 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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [9, 11], [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`));
|
||||||
|
|
||||||
|
assertNestedRangesEqual(ranges![0], [8, 12], [7, 17], [1, 17], [0, 17]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function assertNestedRangesEqual(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++) {
|
||||||
|
assertRangesEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 assertRangesEqual(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;
|
||||||
|
};
|
Loading…
Reference in a new issue