Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@ -0,0 +1,145 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import { joinLines } from './util';
|
||||
|
||||
const testFileA = workspaceFile('a.md');
|
||||
|
||||
function workspaceFile(...segments: string[]) {
|
||||
return vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, ...segments);
|
||||
}
|
||||
|
||||
async function getLinksForFile(file: vscode.Uri): Promise<vscode.DocumentLink[]> {
|
||||
return (await vscode.commands.executeCommand<vscode.DocumentLink[]>('vscode.executeLinkProvider', file))!;
|
||||
}
|
||||
|
||||
suite('Markdown Document links', () => {
|
||||
|
||||
teardown(async () => {
|
||||
await vscode.commands.executeCommand('workbench.action.closeAllEditors');
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file', async () => {
|
||||
await withFileContents(testFileA, '[b](b.md)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file with leading ./', async () => {
|
||||
await withFileContents(testFileA, '[b](./b.md)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file with leading /', async () => {
|
||||
await withFileContents(testFileA, '[b](./b.md)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file without file extension', async () => {
|
||||
await withFileContents(testFileA, '[b](b)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('b.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to markdown file in directory', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/c)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
|
||||
});
|
||||
|
||||
test('Should navigate to fragment by title in file', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/c#second)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
|
||||
test('Should navigate to fragment by line', async () => {
|
||||
await withFileContents(testFileA, '[b](sub/c#L2)');
|
||||
|
||||
const [link] = await getLinksForFile(testFileA);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(workspaceFile('sub', 'c.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
|
||||
test('Should navigate to fragment within current file', async () => {
|
||||
await withFileContents(testFileA, joinLines(
|
||||
'[](a#header)',
|
||||
'[](#header)',
|
||||
'# Header'));
|
||||
|
||||
const links = await getLinksForFile(testFileA);
|
||||
{
|
||||
await executeLink(links[0]);
|
||||
assertActiveDocumentUri(workspaceFile('a.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
|
||||
}
|
||||
{
|
||||
await executeLink(links[1]);
|
||||
assertActiveDocumentUri(workspaceFile('a.md'));
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('Should navigate to fragment within current untitled file', async () => {
|
||||
const testFile = workspaceFile('x.md').with({ scheme: 'untitled' });
|
||||
await withFileContents(testFile, joinLines(
|
||||
'[](#second)',
|
||||
'# Second'));
|
||||
|
||||
const [link] = await getLinksForFile(testFile);
|
||||
await executeLink(link);
|
||||
|
||||
assertActiveDocumentUri(testFile);
|
||||
assert.strictEqual(vscode.window.activeTextEditor!.selection.start.line, 1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function assertActiveDocumentUri(expectedUri: vscode.Uri) {
|
||||
assert.strictEqual(
|
||||
vscode.window.activeTextEditor!.document.uri.fsPath,
|
||||
expectedUri.fsPath
|
||||
);
|
||||
}
|
||||
|
||||
async function withFileContents(file: vscode.Uri, contents: string): Promise<void> {
|
||||
const document = await vscode.workspace.openTextDocument(file);
|
||||
const editor = await vscode.window.showTextDocument(document);
|
||||
await editor.edit(edit => {
|
||||
edit.replace(new vscode.Range(0, 0, 1000, 0), contents);
|
||||
});
|
||||
}
|
||||
|
||||
async function executeLink(link: vscode.DocumentLink) {
|
||||
const args = JSON.parse(decodeURIComponent(link.target!.query));
|
||||
await vscode.commands.executeCommand(link.target!.path, args);
|
||||
}
|
||||
|
@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import LinkProvider from '../features/documentLinkProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
|
||||
|
||||
const testFile = vscode.Uri.joinPath(vscode.workspace.workspaceFolders![0].uri, 'x.md');
|
||||
|
||||
const noopToken = new class implements vscode.CancellationToken {
|
||||
private _onCancellationRequestedEmitter = new vscode.EventEmitter<void>();
|
||||
public onCancellationRequested = this._onCancellationRequestedEmitter.event;
|
||||
|
||||
get isCancellationRequested() { return false; }
|
||||
};
|
||||
|
||||
function getLinksForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFile, fileContents);
|
||||
const provider = new LinkProvider();
|
||||
return provider.provideDocumentLinks(doc, noopToken);
|
||||
}
|
||||
|
||||
function assertRangeEqual(expected: vscode.Range, actual: vscode.Range) {
|
||||
assert.strictEqual(expected.start.line, actual.start.line);
|
||||
assert.strictEqual(expected.start.character, actual.start.character);
|
||||
assert.strictEqual(expected.end.line, actual.end.line);
|
||||
assert.strictEqual(expected.end.character, actual.end.character);
|
||||
}
|
||||
|
||||
suite('markdown.DocumentLinkProvider', () => {
|
||||
test('Should not return anything for empty document', () => {
|
||||
const links = getLinksForFile('');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for simple document without links', () => {
|
||||
const links = getLinksForFile('# a\nfdasfdfsafsa');
|
||||
assert.strictEqual(links.length, 0);
|
||||
});
|
||||
|
||||
test('Should detect basic http links', () => {
|
||||
const links = getLinksForFile('a [b](https://example.com) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 25));
|
||||
});
|
||||
|
||||
test('Should detect basic workspace links', () => {
|
||||
{
|
||||
const links = getLinksForFile('a [b](./file) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 12));
|
||||
}
|
||||
{
|
||||
const links = getLinksForFile('a [b](file.png) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 14));
|
||||
}
|
||||
});
|
||||
|
||||
test('Should detect links with title', () => {
|
||||
const links = getLinksForFile('a [b](https://example.com "abc") c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 25));
|
||||
});
|
||||
|
||||
// #35245
|
||||
test('Should handle links with escaped characters in name', () => {
|
||||
const links = getLinksForFile('a [b\\]](./file)');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 8, 0, 14));
|
||||
});
|
||||
|
||||
|
||||
test('Should handle links with balanced parens', () => {
|
||||
{
|
||||
const links = getLinksForFile('a [b](https://example.com/a()c) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 30));
|
||||
}
|
||||
{
|
||||
const links = getLinksForFile('a [b](https://example.com/a(b)c) c');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 6, 0, 31));
|
||||
|
||||
}
|
||||
{
|
||||
// #49011
|
||||
const links = getLinksForFile('[A link](http://ThisUrlhasParens/A_link(in_parens))');
|
||||
assert.strictEqual(links.length, 1);
|
||||
const [link] = links;
|
||||
assertRangeEqual(link.range, new vscode.Range(0, 9, 0, 50));
|
||||
}
|
||||
});
|
||||
|
||||
test('Should handle two links without space', () => {
|
||||
const links = getLinksForFile('a ([test](test)[test2](test2)) c');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 10, 0, 14));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 23, 0, 28));
|
||||
});
|
||||
|
||||
// #49238
|
||||
test('should handle hyperlinked images', () => {
|
||||
{
|
||||
const links = getLinksForFile('[](https://example.com)');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 13, 0, 22));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 25, 0, 44));
|
||||
}
|
||||
{
|
||||
const links = getLinksForFile('[]( https://whitespace.com )');
|
||||
assert.strictEqual(links.length, 2);
|
||||
const [link1, link2] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 7, 0, 21));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 26, 0, 48));
|
||||
}
|
||||
{
|
||||
const links = getLinksForFile('[](file1.txt) text [](file2.txt)');
|
||||
assert.strictEqual(links.length, 4);
|
||||
const [link1, link2, link3, link4] = links;
|
||||
assertRangeEqual(link1.range, new vscode.Range(0, 6, 0, 14));
|
||||
assertRangeEqual(link2.range, new vscode.Range(0, 17, 0, 26));
|
||||
assertRangeEqual(link3.range, new vscode.Range(0, 39, 0, 47));
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -0,0 +1,97 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import SymbolProvider from '../features/documentSymbolProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
|
||||
function getSymbolsForFile(fileContents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, fileContents);
|
||||
const provider = new SymbolProvider(createNewMarkdownEngine());
|
||||
return provider.provideDocumentSymbols(doc);
|
||||
}
|
||||
|
||||
|
||||
suite('markdown.DocumentSymbolProvider', () => {
|
||||
test('Should not return anything for empty document', async () => {
|
||||
const symbols = await getSymbolsForFile('');
|
||||
assert.strictEqual(symbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for document with no headers', async () => {
|
||||
const symbols = await getSymbolsForFile('a\na');
|
||||
assert.strictEqual(symbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for document with # but no real headers', async () => {
|
||||
const symbols = await getSymbolsForFile('a#a\na#');
|
||||
assert.strictEqual(symbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should return single symbol for single header', async () => {
|
||||
const symbols = await getSymbolsForFile('# h');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
});
|
||||
|
||||
test('Should not care about symbol level for single header', async () => {
|
||||
const symbols = await getSymbolsForFile('### h');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '### h');
|
||||
});
|
||||
|
||||
test('Should put symbols of same level in flat list', async () => {
|
||||
const symbols = await getSymbolsForFile('## h\n## h2');
|
||||
assert.strictEqual(symbols.length, 2);
|
||||
assert.strictEqual(symbols[0].name, '## h');
|
||||
assert.strictEqual(symbols[1].name, '## h2');
|
||||
});
|
||||
|
||||
test('Should nest symbol of level - 1 under parent', async () => {
|
||||
|
||||
const symbols = await getSymbolsForFile('# h\n## h2\n## h3');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
assert.strictEqual(symbols[0].children.length, 2);
|
||||
assert.strictEqual(symbols[0].children[0].name, '## h2');
|
||||
assert.strictEqual(symbols[0].children[1].name, '## h3');
|
||||
});
|
||||
|
||||
test('Should nest symbol of level - n under parent', async () => {
|
||||
const symbols = await getSymbolsForFile('# h\n#### h2');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
assert.strictEqual(symbols[0].children.length, 1);
|
||||
assert.strictEqual(symbols[0].children[0].name, '#### h2');
|
||||
});
|
||||
|
||||
test('Should flatten children where lower level occurs first', async () => {
|
||||
const symbols = await getSymbolsForFile('# h\n### h2\n## h3');
|
||||
assert.strictEqual(symbols.length, 1);
|
||||
assert.strictEqual(symbols[0].name, '# h');
|
||||
assert.strictEqual(symbols[0].children.length, 2);
|
||||
assert.strictEqual(symbols[0].children[0].name, '### h2');
|
||||
assert.strictEqual(symbols[0].children[1].name, '## h3');
|
||||
});
|
||||
|
||||
test('Should handle line separator in file. Issue #63749', async () => {
|
||||
const symbols = await getSymbolsForFile(`# A
|
||||
- foo
|
||||
|
||||
# B
|
||||
- bar`);
|
||||
assert.strictEqual(symbols.length, 2);
|
||||
assert.strictEqual(symbols[0].name, '# A');
|
||||
assert.strictEqual(symbols[1].name, '# B');
|
||||
});
|
||||
});
|
||||
|
@ -0,0 +1,32 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.engine', () => {
|
||||
suite('rendering', () => {
|
||||
const input = '# hello\n\nworld!';
|
||||
const output = '<h1 id="hello" data-line="0" class="code-line">hello</h1>\n'
|
||||
+ '<p data-line="2" class="code-line">world!</p>\n';
|
||||
|
||||
test('Renders a document', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, input);
|
||||
const engine = createNewMarkdownEngine();
|
||||
assert.strictEqual(await engine.render(doc), output);
|
||||
});
|
||||
|
||||
test('Renders a string', async () => {
|
||||
const engine = createNewMarkdownEngine();
|
||||
assert.strictEqual(await engine.render(input), output);
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { MarkdownEngine } from '../markdownEngine';
|
||||
import { MarkdownContributionProvider, MarkdownContributions } from '../markdownExtensions';
|
||||
import { githubSlugifier } from '../slugify';
|
||||
import { Disposable } from '../util/dispose';
|
||||
|
||||
const emptyContributions = new class extends Disposable implements MarkdownContributionProvider {
|
||||
readonly extensionUri = vscode.Uri.file('/');
|
||||
readonly contributions = MarkdownContributions.Empty;
|
||||
readonly onContributionsChanged = this._register(new vscode.EventEmitter<this>()).event;
|
||||
};
|
||||
|
||||
export function createNewMarkdownEngine(): MarkdownEngine {
|
||||
return new MarkdownEngine(emptyContributions, githubSlugifier);
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
|
||||
import MarkdownFoldingProvider from '../features/foldingProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.FoldingProvider', () => {
|
||||
test('Should not return anything for empty document', async () => {
|
||||
const folds = await getFoldsForDocument(``);
|
||||
assert.strictEqual(folds.length, 0);
|
||||
});
|
||||
|
||||
test('Should not return anything for document without headers', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
**b** afas
|
||||
a#b
|
||||
a`);
|
||||
assert.strictEqual(folds.length, 0);
|
||||
});
|
||||
|
||||
test('Should fold from header to end of document', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
# b
|
||||
c
|
||||
d`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('Should leave single newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(`
|
||||
# a
|
||||
x
|
||||
|
||||
# b
|
||||
y`);
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('Should collapse multuple newlines to single newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(`
|
||||
# a
|
||||
x
|
||||
|
||||
|
||||
|
||||
# b
|
||||
y`);
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 5);
|
||||
});
|
||||
|
||||
test('Should not collapse if there is no newline before next header', async () => {
|
||||
const folds = await getFoldsForDocument(`
|
||||
# a
|
||||
x
|
||||
# b
|
||||
y`);
|
||||
assert.strictEqual(folds.length, 2);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should fold nested <!-- #region --> markers', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
<!-- #region -->
|
||||
b
|
||||
<!-- #region hello!-->
|
||||
b.a
|
||||
<!-- #endregion -->
|
||||
b
|
||||
<!-- #region: foo! -->
|
||||
b.b
|
||||
<!-- #endregion: foo -->
|
||||
b
|
||||
<!-- #endregion -->
|
||||
a`);
|
||||
assert.strictEqual(folds.length, 3);
|
||||
const [outer, first, second] = folds.sort((a, b) => a.start - b.start);
|
||||
|
||||
assert.strictEqual(outer.start, 1);
|
||||
assert.strictEqual(outer.end, 11);
|
||||
assert.strictEqual(first.start, 3);
|
||||
assert.strictEqual(first.end, 5);
|
||||
assert.strictEqual(second.start, 7);
|
||||
assert.strictEqual(second.end, 9);
|
||||
});
|
||||
|
||||
test('Should fold from list to end of document', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
- b
|
||||
c
|
||||
d`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('lists folds should span multiple lines of content', async () => {
|
||||
const folds = await getFoldsForDocument(`a
|
||||
- This list item\n spans multiple\n lines.`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('List should leave single blankline before new element', async () => {
|
||||
const folds = await getFoldsForDocument(`- a
|
||||
a
|
||||
|
||||
|
||||
b`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 0);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('Should fold fenced code blocks', async () => {
|
||||
const folds = await getFoldsForDocument(`~~~ts
|
||||
a
|
||||
~~~
|
||||
b`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 0);
|
||||
assert.strictEqual(firstFold.end, 2);
|
||||
});
|
||||
|
||||
test('Should fold fenced code blocks with yaml front matter', async () => {
|
||||
const folds = await getFoldsForDocument(`---
|
||||
title: bla
|
||||
---
|
||||
|
||||
~~~ts
|
||||
a
|
||||
~~~
|
||||
|
||||
a
|
||||
a
|
||||
b
|
||||
a`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 4);
|
||||
assert.strictEqual(firstFold.end, 6);
|
||||
});
|
||||
|
||||
test('Should fold html blocks', async () => {
|
||||
const folds = await getFoldsForDocument(`x
|
||||
<div>
|
||||
fa
|
||||
</div>`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
});
|
||||
|
||||
test('Should fold html block comments', async () => {
|
||||
const folds = await getFoldsForDocument(`x
|
||||
<!--
|
||||
fa
|
||||
-->`);
|
||||
assert.strictEqual(folds.length, 1);
|
||||
const firstFold = folds[0];
|
||||
assert.strictEqual(firstFold.start, 1);
|
||||
assert.strictEqual(firstFold.end, 3);
|
||||
assert.strictEqual(firstFold.kind, vscode.FoldingRangeKind.Comment);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
async function getFoldsForDocument(contents: string) {
|
||||
const doc = new InMemoryDocument(testFileName, contents);
|
||||
const provider = new MarkdownFoldingProvider(createNewMarkdownEngine());
|
||||
return await provider.provideFoldingRanges(doc, {}, new vscode.CancellationTokenSource().token);
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class InMemoryDocument implements vscode.TextDocument {
|
||||
private readonly _lines: string[];
|
||||
|
||||
constructor(
|
||||
public readonly uri: vscode.Uri,
|
||||
private readonly _contents: string,
|
||||
public readonly version = 1,
|
||||
) {
|
||||
this._lines = this._contents.split(/\n/g);
|
||||
}
|
||||
|
||||
|
||||
isUntitled: boolean = false;
|
||||
languageId: string = '';
|
||||
isDirty: boolean = false;
|
||||
isClosed: boolean = false;
|
||||
eol: vscode.EndOfLine = vscode.EndOfLine.LF;
|
||||
notebook: undefined;
|
||||
|
||||
get fileName(): string {
|
||||
return this.uri.fsPath;
|
||||
}
|
||||
|
||||
get lineCount(): number {
|
||||
return this._lines.length;
|
||||
}
|
||||
|
||||
lineAt(line: any): vscode.TextLine {
|
||||
return {
|
||||
lineNumber: line,
|
||||
text: this._lines[line],
|
||||
range: new vscode.Range(0, 0, 0, 0),
|
||||
firstNonWhitespaceCharacterIndex: 0,
|
||||
rangeIncludingLineBreak: new vscode.Range(0, 0, 0, 0),
|
||||
isEmptyOrWhitespace: false
|
||||
};
|
||||
}
|
||||
offsetAt(_position: vscode.Position): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
positionAt(offset: number): vscode.Position {
|
||||
const before = this._contents.slice(0, offset);
|
||||
const newLines = before.match(/\n/g);
|
||||
const line = newLines ? newLines.length : 0;
|
||||
const preCharacters = before.match(/(\n|^).*$/g);
|
||||
return new vscode.Position(line, preCharacters ? preCharacters[0].length : 0);
|
||||
}
|
||||
getText(_range?: vscode.Range | undefined): string {
|
||||
return this._contents;
|
||||
}
|
||||
getWordRangeAtPosition(_position: vscode.Position, _regex?: RegExp | undefined): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
validateRange(_range: vscode.Range): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
validatePosition(_position: vscode.Position): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
save(): never {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
const path = require('path');
|
||||
const testRunner = require('vscode/lib/testrunner');
|
||||
|
||||
const options: any = {
|
||||
ui: 'tdd',
|
||||
useColors: (!process.env.BUILD_ARTIFACTSTAGINGDIRECTORY && process.platform !== 'win32'),
|
||||
timeout: 60000
|
||||
};
|
||||
|
||||
// These integration tests is being run in multiple environments (electron, web, remote)
|
||||
// so we need to set the suite name based on the environment as the suite name is used
|
||||
// for the test results file name
|
||||
let suite = '';
|
||||
if (process.env.VSCODE_BROWSER) {
|
||||
suite = `${process.env.VSCODE_BROWSER} Browser Integration Markdown Tests`;
|
||||
} else if (process.env.REMOTE_VSCODE) {
|
||||
suite = 'Remote Integration Markdown Tests';
|
||||
} else {
|
||||
suite = 'Integration Markdown Tests';
|
||||
}
|
||||
|
||||
if (process.env.BUILD_ARTIFACTSTAGINGDIRECTORY) {
|
||||
options.reporter = 'mocha-multi-reporters';
|
||||
options.reporterOptions = {
|
||||
reporterEnabled: 'spec, mocha-junit-reporter',
|
||||
mochaJunitReporterReporterOptions: {
|
||||
testsuitesTitle: `${suite} ${process.platform}`,
|
||||
mochaFile: path.join(process.env.BUILD_ARTIFACTSTAGINGDIRECTORY, `test-results/${process.platform}-${process.arch}-${suite.toLowerCase().replace(/[^\w]/g, '-')}-results.xml`)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
testRunner.configure(options);
|
||||
|
||||
export = testRunner;
|
@ -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;
|
||||
};
|
@ -0,0 +1,130 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
|
||||
import { TableOfContentsProvider } from '../tableOfContentsProvider';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
suite('markdown.TableOfContentsProvider', () => {
|
||||
test('Lookup should not return anything for empty document', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, '');
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual(await provider.lookup(''), undefined);
|
||||
assert.strictEqual(await provider.lookup('foo'), undefined);
|
||||
});
|
||||
|
||||
test('Lookup should not return anything for document with no headers', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, 'a *b*\nc');
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual(await provider.lookup(''), undefined);
|
||||
assert.strictEqual(await provider.lookup('foo'), undefined);
|
||||
assert.strictEqual(await provider.lookup('a'), undefined);
|
||||
assert.strictEqual(await provider.lookup('b'), undefined);
|
||||
});
|
||||
|
||||
test('Lookup should return basic #header', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# a\nx\n# c`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
{
|
||||
const entry = await provider.lookup('a');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 0);
|
||||
}
|
||||
{
|
||||
assert.strictEqual(await provider.lookup('x'), undefined);
|
||||
}
|
||||
{
|
||||
const entry = await provider.lookup('c');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('Lookups should be case in-sensitive', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# fOo\n`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('fOo'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('foo'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('FOO'))!.line, 0);
|
||||
});
|
||||
|
||||
test('Lookups should ignore leading and trailing white-space, and collapse internal whitespace', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# f o o \n`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup(' f o o'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup(' f o o '))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('f o o'))!.line, 0);
|
||||
|
||||
assert.strictEqual(await provider.lookup('f'), undefined);
|
||||
assert.strictEqual(await provider.lookup('foo'), undefined);
|
||||
assert.strictEqual(await provider.lookup('fo o'), undefined);
|
||||
});
|
||||
|
||||
test('should handle special characters #44779', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# Indentação\n`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('indentação'))!.line, 0);
|
||||
});
|
||||
|
||||
test('should handle special characters 2, #48482', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# Инструкция - Делай Раз, Делай Два\n`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('инструкция---делай-раз-делай-два'))!.line, 0);
|
||||
});
|
||||
|
||||
test('should handle special characters 3, #37079', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `## Header 2
|
||||
### Header 3
|
||||
## Заголовок 2
|
||||
### Заголовок 3
|
||||
### Заголовок Header 3
|
||||
## Заголовок`);
|
||||
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
assert.strictEqual((await provider.lookup('header-2'))!.line, 0);
|
||||
assert.strictEqual((await provider.lookup('header-3'))!.line, 1);
|
||||
assert.strictEqual((await provider.lookup('Заголовок-2'))!.line, 2);
|
||||
assert.strictEqual((await provider.lookup('Заголовок-3'))!.line, 3);
|
||||
assert.strictEqual((await provider.lookup('Заголовок-header-3'))!.line, 4);
|
||||
assert.strictEqual((await provider.lookup('Заголовок'))!.line, 5);
|
||||
});
|
||||
|
||||
test('Lookup should support suffixes for repeated headers', async () => {
|
||||
const doc = new InMemoryDocument(testFileName, `# a\n# a\n## a`);
|
||||
const provider = new TableOfContentsProvider(createNewMarkdownEngine(), doc);
|
||||
|
||||
{
|
||||
const entry = await provider.lookup('a');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 0);
|
||||
}
|
||||
{
|
||||
const entry = await provider.lookup('a-1');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 1);
|
||||
}
|
||||
{
|
||||
const entry = await provider.lookup('a-2');
|
||||
assert.ok(entry);
|
||||
assert.strictEqual(entry!.line, 2);
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
import * as os from 'os';
|
||||
|
||||
export const joinLines = (...args: string[]) =>
|
||||
args.join(os.platform() === 'win32' ? '\r\n' : '\n');
|
@ -0,0 +1,142 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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 'mocha';
|
||||
import * as vscode from 'vscode';
|
||||
import MDDocumentSymbolProvider from '../features/documentSymbolProvider';
|
||||
import MarkdownWorkspaceSymbolProvider, { WorkspaceMarkdownDocumentProvider } from '../features/workspaceSymbolProvider';
|
||||
import { createNewMarkdownEngine } from './engine';
|
||||
import { InMemoryDocument } from './inMemoryDocument';
|
||||
|
||||
|
||||
const symbolProvider = new MDDocumentSymbolProvider(createNewMarkdownEngine());
|
||||
|
||||
suite('markdown.WorkspaceSymbolProvider', () => {
|
||||
test('Should not return anything for empty workspace', async () => {
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([]));
|
||||
|
||||
assert.deepEqual(await provider.provideWorkspaceSymbols(''), []);
|
||||
});
|
||||
|
||||
test('Should return symbols from workspace with one markdown file', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
new InMemoryDocument(testFileName, `# header1\nabc\n## header2`)
|
||||
]));
|
||||
|
||||
const symbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(symbols.length, 2);
|
||||
assert.strictEqual(symbols[0].name, '# header1');
|
||||
assert.strictEqual(symbols[1].name, '## header2');
|
||||
});
|
||||
|
||||
test('Should return all content basic workspace', async () => {
|
||||
const fileNameCount = 10;
|
||||
const files: vscode.TextDocument[] = [];
|
||||
for (let i = 0; i < fileNameCount; ++i) {
|
||||
const testFileName = vscode.Uri.file(`test${i}.md`);
|
||||
files.push(new InMemoryDocument(testFileName, `# common\nabc\n## header${i}`));
|
||||
}
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, new InMemoryWorkspaceMarkdownDocumentProvider(files));
|
||||
|
||||
const symbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(symbols.length, fileNameCount * 2);
|
||||
});
|
||||
|
||||
test('Should update results when markdown file changes symbols', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
new InMemoryDocument(testFileName, `# header1`, 1 /* version */)
|
||||
]);
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// Update file
|
||||
workspaceFileProvider.updateDocument(new InMemoryDocument(testFileName, `# new header\nabc\n## header2`, 2 /* version */));
|
||||
const newSymbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(newSymbols.length, 2);
|
||||
assert.strictEqual(newSymbols[0].name, '# new header');
|
||||
assert.strictEqual(newSymbols[1].name, '## header2');
|
||||
});
|
||||
|
||||
test('Should remove results when file is deleted', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
new InMemoryDocument(testFileName, `# header1`)
|
||||
]);
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// delete file
|
||||
workspaceFileProvider.deleteDocument(testFileName);
|
||||
const newSymbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(newSymbols.length, 0);
|
||||
});
|
||||
|
||||
test('Should update results when markdown file is created', async () => {
|
||||
const testFileName = vscode.Uri.file('test.md');
|
||||
|
||||
const workspaceFileProvider = new InMemoryWorkspaceMarkdownDocumentProvider([
|
||||
new InMemoryDocument(testFileName, `# header1`)
|
||||
]);
|
||||
|
||||
const provider = new MarkdownWorkspaceSymbolProvider(symbolProvider, workspaceFileProvider);
|
||||
assert.strictEqual((await provider.provideWorkspaceSymbols('')).length, 1);
|
||||
|
||||
// Creat file
|
||||
workspaceFileProvider.createDocument(new InMemoryDocument(vscode.Uri.file('test2.md'), `# new header\nabc\n## header2`));
|
||||
const newSymbols = await provider.provideWorkspaceSymbols('');
|
||||
assert.strictEqual(newSymbols.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
class InMemoryWorkspaceMarkdownDocumentProvider implements WorkspaceMarkdownDocumentProvider {
|
||||
private readonly _documents = new Map<string, vscode.TextDocument>();
|
||||
|
||||
constructor(documents: vscode.TextDocument[]) {
|
||||
for (const doc of documents) {
|
||||
this._documents.set(doc.fileName, doc);
|
||||
}
|
||||
}
|
||||
|
||||
async getAllMarkdownDocuments() {
|
||||
return Array.from(this._documents.values());
|
||||
}
|
||||
|
||||
private readonly _onDidChangeMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.TextDocument>();
|
||||
public onDidChangeMarkdownDocument = this._onDidChangeMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidCreateMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.TextDocument>();
|
||||
public onDidCreateMarkdownDocument = this._onDidCreateMarkdownDocumentEmitter.event;
|
||||
|
||||
private readonly _onDidDeleteMarkdownDocumentEmitter = new vscode.EventEmitter<vscode.Uri>();
|
||||
public onDidDeleteMarkdownDocument = this._onDidDeleteMarkdownDocumentEmitter.event;
|
||||
|
||||
public updateDocument(document: vscode.TextDocument) {
|
||||
this._documents.set(document.fileName, document);
|
||||
this._onDidChangeMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public createDocument(document: vscode.TextDocument) {
|
||||
assert.ok(!this._documents.has(document.uri.fsPath));
|
||||
|
||||
this._documents.set(document.uri.fsPath, document);
|
||||
this._onDidCreateMarkdownDocumentEmitter.fire(document);
|
||||
}
|
||||
|
||||
public deleteDocument(resource: vscode.Uri) {
|
||||
this._documents.delete(resource.fsPath);
|
||||
this._onDidDeleteMarkdownDocumentEmitter.fire(resource);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user