Archived
1
0

Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'

This commit is contained in:
Joe Previte
2020-12-15 15:52:33 -07:00
4649 changed files with 1311795 additions and 0 deletions

View File

@ -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);
}

View File

@ -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('[![alt text](image.jpg)](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('[![a]( whitespace.jpg )]( 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('[![a](img1.jpg)](file1.txt) text [![a](img2.jpg)](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);
});
});

View File

@ -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');
});
});

View File

@ -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);
});
});
});

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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.');
}
}

View File

@ -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;

View File

@ -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;
};

View File

@ -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);
}
});
});

View File

@ -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');

View File

@ -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);
}
}