From 08e556dc5df567fbbd342be0c8dcdb182d8f6561 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 4 Nov 2020 10:15:30 -0800 Subject: [PATCH] fix #109486 and #109489 --- .../src/features/smartSelect.ts | 214 ++++++++---------- .../src/test/smartSelect.test.ts | 124 +++++++--- 2 files changed, 184 insertions(+), 154 deletions(-) diff --git a/extensions/markdown-language-features/src/features/smartSelect.ts b/extensions/markdown-language-features/src/features/smartSelect.ts index 0be50d0cae1..2fad25f62d4 100644 --- a/extensions/markdown-language-features/src/features/smartSelect.ts +++ b/extensions/markdown-language-features/src/features/smartSelect.ts @@ -2,7 +2,6 @@ * 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'; @@ -15,7 +14,7 @@ export default class MarkdownSmartSelect implements vscode.SelectionRangeProvide ) { } public async provideSelectionRanges(document: vscode.TextDocument, positions: vscode.Position[], _token: vscode.CancellationToken): Promise { - let promises = await Promise.all(positions.map((position) => { + const promises = await Promise.all(positions.map((position) => { return this.provideSelectionRange(document, position, _token); })); return promises.filter(item => item !== undefined) as vscode.SelectionRange[]; @@ -24,173 +23,142 @@ export default class MarkdownSmartSelect implements vscode.SelectionRangeProvide private async provideSelectionRange(document: vscode.TextDocument, position: vscode.Position, _token: vscode.CancellationToken): Promise { const headerRange = await this.getHeaderSelectionRange(document, position); const blockRange = await this.getBlockSelectionRange(document, position, headerRange); - return blockRange ? blockRange : headerRange ? headerRange : undefined; + return blockRange || headerRange; } private async getBlockSelectionRange(document: vscode.TextDocument, position: vscode.Position, headerRange?: vscode.SelectionRange): Promise { const tokens = await this.engine.parse(document); - let blockTokens = getTokensForPosition(tokens, position); + const 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; + let currentRange: vscode.SelectionRange | undefined = headerRange ? headerRange : createBlockRange(blockTokens.shift()!, document, position.line); - for (const token of blockTokens) { - currentRange = createBlockRange(document, position.line, token, parentRange); - if (currentRange) { - parentRange = currentRange; - } - } - if (currentRange) { - return currentRange; - } else { - return parentRange; + 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 { + const tocProvider = new TableOfContentsProvider(this.engine, document); const toc = await tocProvider.getToc(); - let headerInfo = getHeadersForPosition(toc, position); + const headerInfo = getHeadersForPosition(toc, position); - let headers = headerInfo.headers; + const 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; - } + currentRange = createHeaderRange(headers[i], i === headers.length - 1, headerInfo.headerOnThisLine, currentRange, getFirstChildHeader(document, headers[i], toc)); } 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; + 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 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)))); +function createHeaderRange(header: TocEntry, isClosestHeaderToPosition: boolean, onHeaderLine: boolean, parent?: vscode.SelectionRange, childStart?: vscode.Position): vscode.SelectionRange | undefined { + const contentRange = new vscode.Range(header.location.range.start.translate(1), header.location.range.end); + const headerPlusContentRange = header.location.range; + const 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)); + 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 undefined; + return new vscode.SelectionRange(contentRange, new vscode.SelectionRange(headerPlusContentRange)); } } +function getTokensForPosition(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(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 { +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 startPos = new vscode.Position(startLine, 0); + const endPos = new vscode.Position(endLine, document.lineAt(endLine).text?.length ?? 0); + const 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?.range.isEqual(range)) { + return parent; + } else if (parent) { + // parent doesn't contain range + 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) { + const 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) { + const adjustedRange = new vscode.Range(parent.range.start, range.end.translate(undefined, range.end.character)); + if (adjustedRange.isEqual(parent.range)) { + return parent; + } else { + return new vscode.SelectionRange(adjustedRange, parent.parent); } } else { - return new vscode.SelectionRange(range); + 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; + const onFenceLine = cursorLine === startLine || cursorLine === endLine; + const fenceRange = new vscode.Range(new vscode.Position(startLine, 0), new vscode.Position(endLine, document.lineAt(endLine).text.length)); + const contentRange = endLine - startLine > 2 && !onFenceLine ? new vscode.Range(new vscode.Position(startLine + 1, 0), new vscode.Position(endLine - 1, document.lineAt(endLine - 1).text.length)) : 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; + const 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); @@ -204,7 +172,7 @@ function createFencedRange(token: Token, cursorLine: number, document: vscode.Te } 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; + const 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); @@ -214,15 +182,12 @@ function createFencedRange(token: Token, cursorLine: number, document: vscode.Te 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 isList(token: Token): boolean { + return token.type ? ['ordered_list_open', 'list_item_open', 'bullet_list_open'].includes(token.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 isBlockElement(token: Token): boolean { + return !['list_item_close', 'paragraph_close', 'bullet_list_close', 'inline', 'heading_close', 'heading_open'].includes(token.type); } function getRealParent(parent: vscode.SelectionRange, range: vscode.Range) { @@ -233,6 +198,19 @@ function getRealParent(parent: vscode.SelectionRange, range: vscode.Range) { return currentParent; } +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; +} + function rangeLinesEqual(range: vscode.Range, parent: vscode.Range) { return range.start.line === parent.start.line && range.end.line === parent.end.line; } diff --git a/extensions/markdown-language-features/src/test/smartSelect.test.ts b/extensions/markdown-language-features/src/test/smartSelect.test.ts index 787df59acc3..3849f47cb19 100644 --- a/extensions/markdown-language-features/src/test/smartSelect.test.ts +++ b/extensions/markdown-language-features/src/test/smartSelect.test.ts @@ -14,10 +14,10 @@ const CURSOR = '$$CURSOR$$'; const testFileName = vscode.Uri.file('test.md'); -suite.only('markdown.SmartSelect', () => { +suite('markdown.SmartSelect', () => { test('Smart select single word', async () => { const ranges = await getSelectionRangesForDocument(`Hel${CURSOR}lo`); - assertNestedRangesEqual(ranges![0], [0, 1]); + assertNestedLineNumbersEqual(ranges![0], [0, 0]); }); test('Smart select multi-line paragraph', async () => { const ranges = await getSelectionRangesForDocument( @@ -26,12 +26,12 @@ suite.only('markdown.SmartSelect', () => { `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]); + 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).`); - assertNestedRangesEqual(ranges![0], [0, 1]); + assertNestedLineNumbersEqual(ranges![0], [0, 0]); }); test('Smart select html block', async () => { const ranges = await getSelectionRangesForDocument( @@ -40,7 +40,7 @@ suite.only('markdown.SmartSelect', () => { `${CURSOR}VS Code in action`, `

`)); - assertNestedRangesEqual(ranges![0], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [0, 2]); }); test('Smart select header on header line', async () => { const ranges = await getSelectionRangesForDocument( @@ -48,7 +48,7 @@ suite.only('markdown.SmartSelect', () => { `# Header${CURSOR}`, `Hello`)); - assertNestedRangesEqual(ranges![0], [0, 1]); + assertNestedLineNumbersEqual(ranges![0], [0, 1]); }); test('Smart select single word w grandparent header on text line', async () => { @@ -59,7 +59,7 @@ suite.only('markdown.SmartSelect', () => { `${CURSOR}Hello` )); - assertNestedRangesEqual(ranges![0], [2, 2], [1, 2]); + assertNestedLineNumbersEqual(ranges![0], [2, 2], [1, 2]); }); test('Smart select html block w parent header', async () => { const ranges = await getSelectionRangesForDocument( @@ -69,7 +69,7 @@ suite.only('markdown.SmartSelect', () => { `VS Code in action`, `

`)); - assertNestedRangesEqual(ranges![0], [1, 3], [1, 3], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [1, 1], [1, 3], [0, 3]); }); test('Smart select fenced code block', async () => { const ranges = await getSelectionRangesForDocument( @@ -78,7 +78,7 @@ suite.only('markdown.SmartSelect', () => { `a${CURSOR}`, `~~~`)); - assertNestedRangesEqual(ranges![0], [0, 2]); + assertNestedLineNumbersEqual(ranges![0], [0, 2]); }); test('Smart select list', async () => { const ranges = await getSelectionRangesForDocument( @@ -87,8 +87,7 @@ suite.only('markdown.SmartSelect', () => { `- ${CURSOR}item 2`, `- item 3`, `- item 4`)); - - assertNestedRangesEqual(ranges![0], [1, 1], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [1, 1], [0, 3]); }); test('Smart select list with fenced code block', async () => { const ranges = await getSelectionRangesForDocument( @@ -100,7 +99,7 @@ suite.only('markdown.SmartSelect', () => { `- item 3`, `- item 4`)); - assertNestedRangesEqual(ranges![0], [1, 3], [0, 5]); + assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 5]); }); test('Smart select multi cursor', async () => { const ranges = await getSelectionRangesForDocument( @@ -112,8 +111,8 @@ suite.only('markdown.SmartSelect', () => { `- ${CURSOR}item 3`, `- item 4`)); - assertNestedRangesEqual(ranges![0], [0, 0], [0, 5]); - assertNestedRangesEqual(ranges![1], [4, 4], [0, 5]); + 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( @@ -122,7 +121,7 @@ suite.only('markdown.SmartSelect', () => { `> item 2`, `>> ${CURSOR}item 3`, `>> item 4`)); - assertNestedRangesEqual(ranges![0], [2, 4], [0, 4]); + assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [0, 3]); }); test('Smart select multi nested block quotes', async () => { const ranges = await getSelectionRangesForDocument( @@ -131,8 +130,7 @@ suite.only('markdown.SmartSelect', () => { `>> item 2`, `>>> ${CURSOR}item 3`, `>>>> item 4`)); - - assertNestedRangesEqual(ranges![0], [2, 3], [2, 4], [1, 4], [0, 4]); + assertNestedLineNumbersEqual(ranges![0], [2, 2], [2, 3], [1, 3], [0, 3]); }); test('Smart select subheader content', async () => { const ranges = await getSelectionRangesForDocument( @@ -143,7 +141,7 @@ suite.only('markdown.SmartSelect', () => { `${CURSOR}content 2`, `# main header 2`)); - assertNestedRangesEqual(ranges![0], [3, 3], [2, 3], [1, 3], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [3, 3], [2, 3], [1, 3], [0, 3]); }); test('Smart select subheader line', async () => { const ranges = await getSelectionRangesForDocument( @@ -154,7 +152,7 @@ suite.only('markdown.SmartSelect', () => { `content 2`, `# main header 2`)); - assertNestedRangesEqual(ranges![0], [2, 3], [1, 3], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [2, 3], [1, 3], [0, 3]); }); test('Smart select blank line', async () => { const ranges = await getSelectionRangesForDocument( @@ -165,7 +163,7 @@ suite.only('markdown.SmartSelect', () => { `content 2`, `# main header 2`)); - assertNestedRangesEqual(ranges![0], [1, 3], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [1, 3], [0, 3]); }); test('Smart select line between paragraphs', async () => { const ranges = await getSelectionRangesForDocument( @@ -174,7 +172,7 @@ suite.only('markdown.SmartSelect', () => { `${CURSOR}`, `paragraph 2`)); - assertNestedRangesEqual(ranges![0], [0, 3]); + assertNestedLineNumbersEqual(ranges![0], [0, 2]); }); test('Smart select empty document', async () => { const ranges = await getSelectionRangesForDocument(``, [new vscode.Position(0, 0)]); @@ -196,7 +194,7 @@ suite.only('markdown.SmartSelect', () => { `more content`, `# main header 2`)); - assertNestedRangesEqual(ranges![0], [4, 6], [3, 9], [3, 10], [2, 10], [1, 10], [0, 10]); + 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( @@ -209,8 +207,7 @@ suite.only('markdown.SmartSelect', () => { ``, `content 2`, `# main header 2`)); - - assertNestedRangesEqual(ranges![0], [2, 3], [1, 3], [1, 6], [0, 6]); + 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( @@ -224,7 +221,7 @@ suite.only('markdown.SmartSelect', () => { `content 2`, `# main header 2`)); - assertNestedRangesEqual(ranges![0], [0, 3], [0, 6]); + 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( @@ -242,7 +239,7 @@ suite.only('markdown.SmartSelect', () => { `content 2`, `# main header 2`)); - assertNestedRangesEqual(ranges![0], [4, 6], [2, 6], [1, 7], [1, 10], [0, 10]); + 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( @@ -266,7 +263,7 @@ suite.only('markdown.SmartSelect', () => { `- content 2`, `content 2`)); - assertNestedRangesEqual(ranges![0], [11, 12], [9, 12], [9, 17], [8, 17], [1, 17], [0, 17]); + 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( @@ -288,9 +285,9 @@ suite.only('markdown.SmartSelect', () => { `- content 2`, `- content 2`, `- content 2`, - `${CURSOR}content 2`)); + `- ${CURSOR}content 2`)); - assertNestedRangesEqual(ranges![0], [16, 17], [14, 17], [14, 17], [13, 17], [9, 17], [8, 17], [1, 17], [0, 17]); + 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( @@ -312,9 +309,9 @@ suite.only('markdown.SmartSelect', () => { `- content 2`, `- content 2`, `- content 2`, - `content 2${CURSOR}`)); + `- content 2${CURSOR}`)); - assertNestedRangesEqual(ranges![0], [16, 17], [14, 17], [14, 17], [13, 17], [9, 17], [8, 17], [1, 17], [0, 17]); + 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( @@ -338,7 +335,7 @@ suite.only('markdown.SmartSelect', () => { `- content 2`, `- content 2`)); - assertNestedRangesEqual(ranges![0], [9, 11], [8, 12], [7, 17], [1, 17], [0, 17]); + assertNestedLineNumbersEqual(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( @@ -362,15 +359,70 @@ suite.only('markdown.SmartSelect', () => { `- content 2`, `- content 2`)); - assertNestedRangesEqual(ranges![0], [8, 12], [7, 17], [1, 17], [0, 17]); + 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]); }); }); -function assertNestedRangesEqual(range: vscode.SelectionRange, ...expectedRanges: [number, number][]) { +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++) { - assertRangesEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`); + assertLineNumbersEqual(lineage[i], expectedRanges[i][0], expectedRanges[i][1], `parent at a depth of ${i}`); } } @@ -384,7 +436,7 @@ function getLineage(range: vscode.SelectionRange): vscode.SelectionRange[] { return result; } -function assertRangesEqual(selectionRange: vscode.SelectionRange, startLine: number, endLine: number, message: string) { +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}`); }